KCMUtils

kpluginwidget.cpp
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 layout->setSpacing(0);
56
57 // Adding content margins on a QLineEdit breaks inline actions
58 auto lineEditWrapper = new QWidget(this);
59 auto lineEditWrapperLayout = new QVBoxLayout(lineEditWrapper);
60 lineEditWrapperLayout->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
61 style()->pixelMetric(QStyle::PM_LayoutTopMargin),
62 style()->pixelMetric(QStyle::PM_LayoutRightMargin),
63 style()->pixelMetric(QStyle::PM_LayoutBottomMargin));
64
65 d->lineEdit = new QLineEdit(lineEditWrapper);
66 d->lineEdit->setClearButtonEnabled(true);
67 d->lineEdit->setPlaceholderText(i18n("Search..."));
68 lineEditWrapperLayout->addWidget(d->lineEdit);
69 d->listView = new KCategorizedView(this);
70 d->listView->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge}));
71 d->categoryDrawer = new KCategoryDrawer(d->listView);
72 d->listView->setVerticalScrollMode(QListView::ScrollPerPixel);
73 d->listView->setAlternatingRowColors(true);
74 d->listView->setCategoryDrawer(d->categoryDrawer);
75
76 d->pluginModel = new KPluginModel(this);
77
78 connect(d->pluginModel, &KPluginModel::defaulted, this, &KPluginWidget::defaulted);
79 connect(d->pluginModel,
81 this,
82 [this](const QModelIndex &topLeft, const QModelIndex & /*bottomRight*/, const QList<int> &roles) {
83 if (roles.contains(KPluginModel::EnabledRole)) {
84 Q_EMIT pluginEnabledChanged(topLeft.data(KPluginModel::IdRole).toString(), topLeft.data(KPluginModel::EnabledRole).toBool());
85 Q_EMIT changed(d->pluginModel->isSaveNeeded());
86 }
87 });
88
89 d->proxyModel = new KPluginProxyModel(this);
90 d->proxyModel->setModel(d->pluginModel);
91 d->listView->setModel(d->proxyModel);
92 d->listView->setAlternatingRowColors(true);
93
94 auto pluginDelegate = new PluginDelegate(d.get(), this);
95 d->listView->setItemDelegate(pluginDelegate);
96
97 d->listView->setMouseTracking(true);
98 d->listView->viewport()->setAttribute(Qt::WA_Hover);
99
100 connect(d->lineEdit, &QLineEdit::textChanged, d->proxyModel, [this](const QString &query) {
101 d->proxyModel->setProperty("query", query);
102 d->proxyModel->invalidate();
103 });
104 connect(pluginDelegate, &PluginDelegate::configCommitted, this, &KPluginWidget::pluginConfigSaved);
105 connect(pluginDelegate, &PluginDelegate::changed, this, &KPluginWidget::pluginEnabledChanged);
106
107 layout->addWidget(lineEditWrapper);
108 layout->addWidget(d->listView);
109
110 // When a KPluginWidget instance gets focus,
111 // it should pass over the focus to its child searchbar.
112 setFocusProxy(d->lineEdit);
113}
114
115KPluginWidget::~KPluginWidget()
116{
117 delete d->listView->itemDelegate();
118 delete d->listView; // depends on some other things in d, make sure this dies first.
119}
120
121void KPluginWidget::addPlugins(const QList<KPluginMetaData> &plugins, const QString &categoryLabel)
122{
123 d->pluginModel->addPlugins(plugins, categoryLabel);
124 d->proxyModel->sort(0);
125}
126
128{
129 d->pluginModel->setConfig(config);
130}
131
133{
134 d->pluginModel->clear();
135}
136
138{
139 d->pluginModel->save();
140}
141
143{
144 d->pluginModel->load();
145}
146
148{
149 d->pluginModel->defaults();
150}
151
153{
154 for (int i = 0, count = d->pluginModel->rowCount(); i < count; ++i) {
155 const QModelIndex index = d->pluginModel->index(i, 0);
156 if (d->pluginModel->data(index, Qt::CheckStateRole).toBool() != d->pluginModel->data(index, KPluginModel::EnabledByDefaultRole).toBool()) {
157 return false;
158 }
159 }
160
161 return true;
162}
163
165{
166 return d->pluginModel->isSaveNeeded();
167}
168
169void KPluginWidget::setConfigurationArguments(const QVariantList &arguments)
170{
171 d->kcmArguments = arguments;
172}
173
175{
176 return d->kcmArguments;
177}
178
180{
182 for (int i = 0, c = d->proxyModel->rowCount(); i < c; ++i) {
183 const auto currentIndex = d->proxyModel->index(i, 0);
184 const QString id = currentIndex.data(KPluginModel::IdRole).toString();
185 if (id == pluginId) {
186 idx = currentIndex;
187 break;
188 }
189 }
190
191 if (idx.isValid()) {
192 auto delegate = static_cast<PluginDelegate *>(d->listView->itemDelegate());
193 delegate->configure(idx);
194 } else {
195 qCWarning(KCMUTILS_LOG) << "Could not find plugin" << pluginId;
196 }
197}
198
200{
201 auto delegate = static_cast<PluginDelegate *>(d->listView->itemDelegate());
202 delegate->resetModel();
203
204 d->showDefaultIndicator = isVisible;
205}
206
207void KPluginWidget::setAdditionalButtonHandler(const std::function<QPushButton *(const KPluginMetaData &)> &handler)
208{
209 auto delegate = static_cast<PluginDelegate *>(d->listView->itemDelegate());
210 delegate->handler = handler;
211}
212
213PluginDelegate::PluginDelegate(KPluginWidgetPrivate *pluginSelector_d_ptr, QObject *parent)
214 : KWidgetItemDelegate(pluginSelector_d_ptr->listView, parent)
215 , checkBox(new QCheckBox)
216 , pushButton(new QPushButton)
217 , pluginSelector_d(pluginSelector_d_ptr)
218{
219 // set the icon to make sure the size can be properly calculated
220 pushButton->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
221}
222
223PluginDelegate::~PluginDelegate()
224{
225 delete checkBox;
226 delete pushButton;
227}
228
229void PluginDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
230{
231 if (!index.isValid()) {
232 return;
233 }
234
235 const int xOffset = checkBox->sizeHint().width();
236 const bool disabled = !index.model()->data(index, KPluginModel::IsChangeableRole).toBool();
237
238 painter->save();
239
241
242 const int iconSize = option.rect.height() - (s_margin * 2);
243 QIcon icon = QIcon::fromTheme(index.model()->data(index, Qt::DecorationRole).toString());
244 icon.paint(painter,
245 QRect(pluginSelector_d->dependantLayoutValue(s_margin + option.rect.left() + xOffset, iconSize, option.rect.width()),
246 s_margin + option.rect.top(),
247 iconSize,
248 iconSize));
249
250 QRect contentsRect(pluginSelector_d->dependantLayoutValue(s_margin * 2 + iconSize + option.rect.left() + xOffset,
251 option.rect.width() - (s_margin * 3) - iconSize - xOffset,
252 option.rect.width()),
253 s_margin + option.rect.top(),
254 option.rect.width() - (s_margin * 3) - iconSize - xOffset,
255 option.rect.height() - (s_margin * 2));
256
257 int lessHorizontalSpace = s_margin * 2 + pushButton->sizeHint().width();
258 if (index.model()->data(index, KPluginModel::ConfigRole).value<KPluginMetaData>().isValid()) {
259 lessHorizontalSpace += s_margin + pushButton->sizeHint().width();
260 }
261 // Reserve space for extra button
262 if (handler) {
263 lessHorizontalSpace += s_margin + pushButton->sizeHint().width();
264 }
265
266 contentsRect.setWidth(contentsRect.width() - lessHorizontalSpace);
267
268 if (option.state & QStyle::State_Selected) {
269 painter->setPen(option.palette.highlightedText().color());
270 }
271
272 if (pluginSelector_d->listView->layoutDirection() == Qt::RightToLeft) {
273 contentsRect.translate(lessHorizontalSpace, 0);
274 }
275
276 painter->save();
277 if (disabled) {
278 QPalette pal(option.palette);
279 pal.setCurrentColorGroup(QPalette::Disabled);
280 painter->setPen(pal.text().color());
281 }
282
283 painter->save();
284 QFont font = titleFont(option.font);
285 QFontMetrics fmTitle(font);
286 painter->setFont(font);
287 painter->drawText(contentsRect,
289 fmTitle.elidedText(index.model()->data(index, Qt::DisplayRole).toString(), Qt::ElideRight, contentsRect.width()));
290 painter->restore();
291
292 painter->drawText(
293 contentsRect,
295 option.fontMetrics.elidedText(index.model()->data(index, KPluginModel::DescriptionRole).toString(), Qt::ElideRight, contentsRect.width()));
296
297 painter->restore();
298 painter->restore();
299}
300
301QSize PluginDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
302{
303 int i = 5;
304 int j = 1;
305 if (index.model()->data(index, KPluginModel::ConfigRole).value<KPluginMetaData>().isValid()) {
306 i = 6;
307 j = 2;
308 }
309 // Reserve space for extra button
310 if (handler) {
311 ++j;
312 }
313
314 const QFont font = titleFont(option.font);
315 const QFontMetrics fmTitle(font);
316 const QString text = index.model()->data(index, Qt::DisplayRole).toString();
317 const QString comment = index.model()->data(index, KPluginModel::DescriptionRole).toString();
318 const int maxTextWidth = qMax(fmTitle.boundingRect(text).width(), option.fontMetrics.boundingRect(comment).width());
319
320 const auto iconSize = pluginSelector_d->listView->style()->pixelMetric(QStyle::PM_IconViewIconSize);
321 return QSize(maxTextWidth + iconSize + s_margin * i + pushButton->sizeHint().width() * j,
322 qMax(iconSize + s_margin * 2, fmTitle.height() + option.fontMetrics.height() + s_margin * 2));
323}
324
325QList<QWidget *> PluginDelegate::createItemWidgets(const QModelIndex &index) const
326{
327 Q_UNUSED(index);
328 QList<QWidget *> widgetList;
329
330 auto enabledCheckBox = new QCheckBox;
331 connect(enabledCheckBox, &QAbstractButton::clicked, this, &PluginDelegate::slotStateChanged);
332
333 auto aboutPushButton = new QPushButton;
334 aboutPushButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information")));
335 aboutPushButton->setToolTip(i18n("About"));
336 connect(aboutPushButton, &QAbstractButton::clicked, this, &PluginDelegate::slotAboutClicked);
337
338 auto configurePushButton = new QPushButton;
339 configurePushButton->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
340 configurePushButton->setToolTip(i18n("Configure"));
341 connect(configurePushButton, &QAbstractButton::clicked, this, &PluginDelegate::slotConfigureClicked);
342
343 const static QList<QEvent::Type> blockedEvents{
349 };
350 setBlockedEventTypes(enabledCheckBox, blockedEvents);
351
352 setBlockedEventTypes(aboutPushButton, blockedEvents);
353
354 setBlockedEventTypes(configurePushButton, blockedEvents);
355
356 widgetList << enabledCheckBox << aboutPushButton << configurePushButton;
357 if (handler) {
358 QPushButton *btn = handler(pluginSelector_d->pluginModel->data(index, KPluginModel::MetaDataRole).value<KPluginMetaData>());
359 if (btn) {
360 widgetList << btn;
361 }
362 }
363
364 return widgetList;
365}
366
367void PluginDelegate::updateItemWidgets(const QList<QWidget *> &widgets, const QStyleOptionViewItem &option, const QPersistentModelIndex &index) const
368{
369 int extraButtonWidth = 0;
370 QPushButton *extraButton = nullptr;
371 if (widgets.count() == 4) {
372 extraButton = static_cast<QPushButton *>(widgets[3]);
373 extraButtonWidth = extraButton->sizeHint().width() + s_margin;
374 }
375 auto checkBox = static_cast<QCheckBox *>(widgets[0]);
376 checkBox->resize(checkBox->sizeHint());
377 checkBox->move(pluginSelector_d->dependantLayoutValue(s_margin, checkBox->sizeHint().width(), option.rect.width()),
378 option.rect.height() / 2 - checkBox->sizeHint().height() / 2);
379
380 auto aboutPushButton = static_cast<QPushButton *>(widgets[1]);
381 const QSize aboutPushButtonSizeHint = aboutPushButton->sizeHint();
382 aboutPushButton->resize(aboutPushButtonSizeHint);
383 aboutPushButton->move(pluginSelector_d->dependantLayoutValue(option.rect.width() - s_margin - aboutPushButtonSizeHint.width() - extraButtonWidth,
384 aboutPushButtonSizeHint.width(),
385 option.rect.width()),
386 option.rect.height() / 2 - aboutPushButtonSizeHint.height() / 2);
387
388 auto configurePushButton = static_cast<QPushButton *>(widgets[2]);
389 const QSize configurePushButtonSizeHint = configurePushButton->sizeHint();
390 configurePushButton->resize(configurePushButtonSizeHint);
391 configurePushButton->move(pluginSelector_d->dependantLayoutValue(option.rect.width() - s_margin * 2 - configurePushButtonSizeHint.width()
392 - aboutPushButtonSizeHint.width() - extraButtonWidth,
393 configurePushButtonSizeHint.width(),
394 option.rect.width()),
395 option.rect.height() / 2 - configurePushButtonSizeHint.height() / 2);
396
397 if (extraButton) {
398 const QSize extraPushButtonSizeHint = extraButton->sizeHint();
399 extraButton->resize(extraPushButtonSizeHint);
400 extraButton->move(pluginSelector_d->dependantLayoutValue(option.rect.width() - extraButtonWidth, extraPushButtonSizeHint.width(), option.rect.width()),
401 option.rect.height() / 2 - extraPushButtonSizeHint.height() / 2);
402 }
403
404 if (!index.isValid() || !index.internalPointer()) {
405 checkBox->setVisible(false);
406 aboutPushButton->setVisible(false);
407 configurePushButton->setVisible(false);
408 if (extraButton) {
409 extraButton->setVisible(false);
410 }
411 } else {
412 const bool enabledByDefault = index.model()->data(index, KPluginModel::EnabledByDefaultRole).toBool();
413 const bool enabled = index.model()->data(index, KPluginModel::EnabledRole).toBool();
414 checkBox->setProperty("_kde_highlight_neutral", pluginSelector_d->showDefaultIndicator && enabledByDefault != enabled);
415 checkBox->setChecked(index.model()->data(index, Qt::CheckStateRole).toBool());
416 checkBox->setEnabled(index.model()->data(index, KPluginModel::IsChangeableRole).toBool());
417 configurePushButton->setVisible(index.model()->data(index, KPluginModel::ConfigRole).value<KPluginMetaData>().isValid());
418 configurePushButton->setEnabled(index.model()->data(index, Qt::CheckStateRole).toBool());
419 }
420}
421
422void PluginDelegate::slotStateChanged(bool state)
423{
424 if (!focusedIndex().isValid()) {
425 return;
426 }
427
428 QModelIndex index = focusedIndex();
429
430 const_cast<QAbstractItemModel *>(index.model())->setData(index, state, Qt::CheckStateRole);
431}
432
433void PluginDelegate::slotAboutClicked()
434{
435 const QModelIndex index = focusedIndex();
436
437 auto pluginMetaData = index.data(KPluginModel::MetaDataRole).value<KPluginMetaData>();
438
439 auto *aboutPlugin = new KAboutPluginDialog(pluginMetaData, itemView());
440 aboutPlugin->setAttribute(Qt::WA_DeleteOnClose);
441 aboutPlugin->show();
442}
443
444void PluginDelegate::slotConfigureClicked()
445{
446 configure(focusedIndex());
447}
448
449void PluginDelegate::configure(const QModelIndex &index)
450{
451 const QAbstractItemModel *model = index.model();
452 const auto kcm = model->data(index, KPluginModel::ConfigRole).value<KPluginMetaData>();
453
454 auto configDialog = new QDialog(itemView());
455 configDialog->setAttribute(Qt::WA_DeleteOnClose);
456 configDialog->setModal(true);
457 configDialog->setWindowTitle(model->data(index, KPluginModel::NameRole).toString());
458
459 QWidget *kcmWrapper = new QWidget;
460 auto kcmInstance = KCModuleLoader::loadModule(kcm, kcmWrapper, pluginSelector_d->kcmArguments);
461
462 auto layout = new QVBoxLayout(configDialog);
463 layout->addWidget(kcmWrapper);
464
465 auto buttonBox = new QDialogButtonBox(configDialog);
470 connect(buttonBox, &QDialogButtonBox::accepted, configDialog, &QDialog::accept);
471 connect(buttonBox, &QDialogButtonBox::rejected, configDialog, &QDialog::reject);
472 connect(configDialog, &QDialog::accepted, this, [kcmInstance, this, model, index]() {
473 Q_EMIT configCommitted(model->data(index, KPluginModel::IdRole).toString());
474 kcmInstance->save();
475 });
476 connect(configDialog, &QDialog::rejected, this, [kcmInstance]() {
477 kcmInstance->load();
478 });
479
480 connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, [kcmInstance] {
481 kcmInstance->defaults();
482 });
483 layout->addWidget(buttonBox);
484
485 // Load KCM right before showing it
486 kcmInstance->load();
487 configDialog->show();
488}
489
490QFont PluginDelegate::titleFont(const QFont &baseFont) const
491{
492 QFont retFont(baseFont);
493 retFont.setBold(true);
494
495 return retFont;
496}
497
498#include "moc_kpluginwidget.cpp"
499#include "moc_kpluginwidget_p.cpp"
static void assign(QPushButton *button, const KGuiItem &item)
bool isValid() const
void save()
Saves the changes to the config set by setConfig.
QVariantList configurationArguments() const
Returns the configuration arguments that will be used.
void load()
Loads the enabled state of the plugins from the config set by setConfig() and clears any changes by t...
void setConfig(const KConfigGroup &config)
Set the config object that will be used to store the enabled state of the plugins.
void setConfigurationArguments(const QVariantList &arguments)
Sets the arguments with which the configuration modules will be initialized.
void setAdditionalButtonHandler(const std::function< QPushButton *(const KPluginMetaData &)> &handler)
Add additional widgets to each row of the plugin selector.
void addPlugins(const QList< KPluginMetaData > &plugins, const QString &categoryLabel)
Adds the plugins with the given label to the widget.
bool isSaveNeeded() const
Returns true if the plugin selector has any changes that are not yet saved to configuration.
void pluginConfigSaved(const QString &pluginId)
Emitted after the config of an embedded KCM has been saved.
void showConfiguration(const QString &pluginId)
Shows the configuration dialog for the plugin pluginId if it's available.
void setDefaultsIndicatorsVisible(bool isVisible)
Shows an indicator when a plugin status is different from default.
void defaulted(bool isDefault)
Emitted after configuration is changed.
void defaults()
Resets the enabled state of the plugins to their defaults.
bool isDefault() const
Returns true if the enabled state of each plugin is the same as that plugin's default state.
void pluginEnabledChanged(const QString &pluginId, bool enabled)
Emitted when any of the plugins are changed.
void clear()
Clears all the added plugins and any unsaved changes.
QString i18n(const char *text, const TYPE &arg...)
KCMUTILS_EXPORT KCModule * loadModule(const KPluginMetaData &metaData, QWidget *parent=nullptr, const QVariantList &args={}, const std::shared_ptr< QQmlEngine > &engine={})
Loads a KCModule.
bool isValid(QStringView ifopt)
KGuiItem configure()
KGuiItem cancel()
KGuiItem defaults()
void clicked(bool checked)
void setIcon(const QIcon &icon)
virtual QVariant data(const QModelIndex &index, int role) const const=0
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
QStyle * style()
virtual void accept()
void accepted()
virtual void reject()
void rejected()
MouseButtonPress
QIcon fromTheme(const QString &name)
void paint(QPainter *painter, const QRect &rect, Qt::Alignment alignment, Mode mode, State state) const const
void textChanged(const QString &text)
qsizetype count() const const
QVariant data(int role) const const
bool isValid() const const
const QAbstractItemModel * model() const const
QList< T > findChildren(Qt::FindChildOptions options) const const
void drawText(const QPoint &position, const QString &text)
void restore()
void save()
void setFont(const QFont &font)
void setPen(Qt::PenStyle style)
bool isValid() const const
const QAbstractItemModel * model() const const
virtual QSize sizeHint() const const override
int height() const const
int width() const const
QChar * data()
PM_LayoutLeftMargin
PE_PanelItemViewItem
virtual void drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const const=0
AlignLeft
CheckStateRole
LeftToRight
ElideRight
WA_Hover
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QVariant fromValue(T &&value)
bool toBool() const const
QString toString() const const
T value() const const
void move(const QPoint &)
void resize(const QSize &)
bool isVisible() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:17:56 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.