KCMUtils

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

KDE's Doxygen guidelines are available online.