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 | |
38 | static constexpr int s_margin = 5; |
39 | |
40 | int 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 | |
49 | KPluginWidget::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 | |
104 | KPluginWidget::~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 | |
110 | void KPluginWidget::addPlugins(const QList<KPluginMetaData> &plugins, const QString &categoryLabel) |
111 | { |
112 | d->pluginModel->addPlugins(plugins, categoryLabel); |
113 | d->proxyModel->sort(0); |
114 | } |
115 | |
116 | void KPluginWidget::setConfig(const KConfigGroup &config) |
117 | { |
118 | d->pluginModel->setConfig(config); |
119 | } |
120 | |
121 | void KPluginWidget::clear() |
122 | { |
123 | d->pluginModel->clear(); |
124 | } |
125 | |
126 | void KPluginWidget::save() |
127 | { |
128 | d->pluginModel->save(); |
129 | } |
130 | |
131 | void KPluginWidget::load() |
132 | { |
133 | d->pluginModel->load(); |
134 | } |
135 | |
136 | void KPluginWidget::defaults() |
137 | { |
138 | d->pluginModel->defaults(); |
139 | } |
140 | |
141 | bool 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 | |
153 | bool KPluginWidget::isSaveNeeded() const |
154 | { |
155 | return d->pluginModel->isSaveNeeded(); |
156 | } |
157 | |
158 | void KPluginWidget::setConfigurationArguments(const QVariantList &arguments) |
159 | { |
160 | d->kcmArguments = arguments; |
161 | } |
162 | |
163 | QVariantList KPluginWidget::configurationArguments() const |
164 | { |
165 | return d->kcmArguments; |
166 | } |
167 | |
168 | void 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 | |
188 | void KPluginWidget::setDefaultsIndicatorsVisible(bool isVisible) |
189 | { |
190 | auto delegate = static_cast<PluginDelegate *>(d->listView->itemDelegate()); |
191 | delegate->resetModel(); |
192 | |
193 | d->showDefaultIndicator = isVisible; |
194 | } |
195 | |
196 | void 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 | |
202 | PluginDelegate::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 | |
212 | PluginDelegate::~PluginDelegate() |
213 | { |
214 | delete checkBox; |
215 | delete pushButton; |
216 | } |
217 | |
218 | void 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 | |
290 | QSize 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 = 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 | |
314 | QList<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 | |
356 | void PluginDelegate::updateItemWidgets(const QList<QWidget *> &widgets, const QStyleOptionViewItem &option, const QPersistentModelIndex &index) const |
357 | { |
358 | int = 0; |
359 | QPushButton * = 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 = 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 | |
411 | void 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 | |
422 | void 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 | |
433 | void PluginDelegate::slotConfigureClicked() |
434 | { |
435 | configure(focusedIndex()); |
436 | } |
437 | |
438 | void 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 | |
479 | QFont 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 | |