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