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