/* SPDX-FileCopyrightText: 2021 Nicolas Fella SPDX-FileCopyrightText: 2021 Alexander Lohnau SPDX-License-Identifier: LGPL-2.0-or-later */ #include "kpluginwidget.h" #include "kcmoduleloader.h" #include "kpluginproxymodel.h" #include "kpluginwidget_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static constexpr int s_margin = 5; int KPluginWidgetPrivate::dependantLayoutValue(int value, int width, int totalWidth) const { if (listView->layoutDirection() == Qt::LeftToRight) { return value; } return totalWidth - width - value; } KPluginWidget::KPluginWidget(QWidget *parent) : QWidget(parent) , d(new KPluginWidgetPrivate) { auto layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); // Adding content margins on a QLineEdit breaks inline actions auto lineEditWrapper = new QWidget(this); auto lineEditWrapperLayout = new QVBoxLayout(lineEditWrapper); lineEditWrapperLayout->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin), style()->pixelMetric(QStyle::PM_LayoutTopMargin), style()->pixelMetric(QStyle::PM_LayoutRightMargin), style()->pixelMetric(QStyle::PM_LayoutBottomMargin)); d->lineEdit = new QLineEdit(lineEditWrapper); d->lineEdit->setClearButtonEnabled(true); d->lineEdit->setPlaceholderText(i18n("Search…")); lineEditWrapperLayout->addWidget(d->lineEdit); d->listView = new KCategorizedView(this); d->listView->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge})); d->categoryDrawer = new KCategoryDrawer(d->listView); d->listView->setVerticalScrollMode(QListView::ScrollPerPixel); d->listView->setAlternatingRowColors(true); d->listView->setCategoryDrawer(d->categoryDrawer); d->pluginModel = new KPluginModel(this); connect(d->pluginModel, &KPluginModel::defaulted, this, &KPluginWidget::defaulted); connect(d->pluginModel, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &topLeft, const QModelIndex & /*bottomRight*/, const QList &roles) { if (roles.contains(KPluginModel::EnabledRole)) { Q_EMIT pluginEnabledChanged(topLeft.data(KPluginModel::IdRole).toString(), topLeft.data(KPluginModel::EnabledRole).toBool()); Q_EMIT changed(d->pluginModel->isSaveNeeded()); } }); d->proxyModel = new KPluginProxyModel(this); d->proxyModel->setModel(d->pluginModel); d->listView->setModel(d->proxyModel); d->listView->setAlternatingRowColors(true); auto pluginDelegate = new PluginDelegate(d.get(), this); d->listView->setItemDelegate(pluginDelegate); d->listView->setMouseTracking(true); d->listView->viewport()->setAttribute(Qt::WA_Hover); connect(d->lineEdit, &QLineEdit::textChanged, d->proxyModel, [this](const QString &query) { d->proxyModel->setProperty("query", query); d->proxyModel->invalidate(); }); connect(pluginDelegate, &PluginDelegate::configCommitted, this, &KPluginWidget::pluginConfigSaved); connect(pluginDelegate, &PluginDelegate::changed, this, &KPluginWidget::pluginEnabledChanged); layout->addWidget(lineEditWrapper); layout->addWidget(d->listView); // When a KPluginWidget instance gets focus, // it should pass over the focus to its child searchbar. setFocusProxy(d->lineEdit); } KPluginWidget::~KPluginWidget() { delete d->listView->itemDelegate(); delete d->listView; // depends on some other things in d, make sure this dies first. } void KPluginWidget::addPlugins(const QList &plugins, const QString &categoryLabel) { d->pluginModel->addPlugins(plugins, categoryLabel); d->proxyModel->sort(0); } void KPluginWidget::setConfig(const KConfigGroup &config) { d->pluginModel->setConfig(config); } void KPluginWidget::clear() { d->pluginModel->clear(); } void KPluginWidget::save() { d->pluginModel->save(); } void KPluginWidget::load() { d->pluginModel->load(); } void KPluginWidget::defaults() { d->pluginModel->defaults(); } bool KPluginWidget::isDefault() const { for (int i = 0, count = d->pluginModel->rowCount(); i < count; ++i) { const QModelIndex index = d->pluginModel->index(i, 0); if (d->pluginModel->data(index, Qt::CheckStateRole).toBool() != d->pluginModel->data(index, KPluginModel::EnabledByDefaultRole).toBool()) { return false; } } return true; } bool KPluginWidget::isSaveNeeded() const { return d->pluginModel->isSaveNeeded(); } void KPluginWidget::setConfigurationArguments(const QVariantList &arguments) { d->kcmArguments = arguments; } QVariantList KPluginWidget::configurationArguments() const { return d->kcmArguments; } void KPluginWidget::showConfiguration(const QString &pluginId) { QModelIndex idx; for (int i = 0, c = d->proxyModel->rowCount(); i < c; ++i) { const auto currentIndex = d->proxyModel->index(i, 0); const QString id = currentIndex.data(KPluginModel::IdRole).toString(); if (id == pluginId) { idx = currentIndex; break; } } if (idx.isValid()) { auto delegate = static_cast(d->listView->itemDelegate()); delegate->configure(idx); } else { qCWarning(KCMUTILS_LOG) << "Could not find plugin" << pluginId; } } void KPluginWidget::setDefaultsIndicatorsVisible(bool isVisible) { auto delegate = static_cast(d->listView->itemDelegate()); delegate->resetModel(); d->showDefaultIndicator = isVisible; } void KPluginWidget::setAdditionalButtonHandler(const std::function &handler) { auto delegate = static_cast(d->listView->itemDelegate()); delegate->handler = handler; } PluginDelegate::PluginDelegate(KPluginWidgetPrivate *pluginSelector_d_ptr, QObject *parent) : KWidgetItemDelegate(pluginSelector_d_ptr->listView, parent) , checkBox(new QCheckBox) , pushButton(new QPushButton) , pluginSelector_d(pluginSelector_d_ptr) { // set the icon to make sure the size can be properly calculated pushButton->setIcon(QIcon::fromTheme(QStringLiteral("configure-symbolic"))); } PluginDelegate::~PluginDelegate() { delete checkBox; delete pushButton; } void PluginDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (!index.isValid()) { return; } const int xOffset = checkBox->sizeHint().width(); const bool disabled = !index.model()->data(index, KPluginModel::IsChangeableRole).toBool(); painter->save(); QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, nullptr); const int iconSize = option.rect.height() - (s_margin * 2); QIcon icon = QIcon::fromTheme(index.model()->data(index, Qt::DecorationRole).toString()); icon.paint(painter, QRect(pluginSelector_d->dependantLayoutValue(s_margin + option.rect.left() + xOffset, iconSize, option.rect.width()), s_margin + option.rect.top(), iconSize, iconSize)); QRect contentsRect(pluginSelector_d->dependantLayoutValue(s_margin * 2 + iconSize + option.rect.left() + xOffset, option.rect.width() - (s_margin * 3) - iconSize - xOffset, option.rect.width()), s_margin + option.rect.top(), option.rect.width() - (s_margin * 3) - iconSize - xOffset, option.rect.height() - (s_margin * 2)); int lessHorizontalSpace = s_margin * 2 + pushButton->sizeHint().width(); if (index.model()->data(index, KPluginModel::ConfigRole).value().isValid()) { lessHorizontalSpace += s_margin + pushButton->sizeHint().width(); } // Reserve space for extra button if (handler) { lessHorizontalSpace += s_margin + pushButton->sizeHint().width(); } contentsRect.setWidth(contentsRect.width() - lessHorizontalSpace); if (option.state & QStyle::State_Selected) { painter->setPen(option.palette.highlightedText().color()); } if (pluginSelector_d->listView->layoutDirection() == Qt::RightToLeft) { contentsRect.translate(lessHorizontalSpace, 0); } painter->save(); if (disabled) { QPalette pal(option.palette); pal.setCurrentColorGroup(QPalette::Disabled); painter->setPen(pal.text().color()); } painter->save(); QFont font = titleFont(option.font); QFontMetrics fmTitle(font); painter->setFont(font); painter->drawText(contentsRect, Qt::AlignLeft | Qt::AlignTop, fmTitle.elidedText(index.model()->data(index, Qt::DisplayRole).toString(), Qt::ElideRight, contentsRect.width())); painter->restore(); painter->drawText( contentsRect, Qt::AlignLeft | Qt::AlignBottom, option.fontMetrics.elidedText(index.model()->data(index, KPluginModel::DescriptionRole).toString(), Qt::ElideRight, contentsRect.width())); painter->restore(); painter->restore(); } QSize PluginDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { int i = 5; int j = 1; if (index.model()->data(index, KPluginModel::ConfigRole).value().isValid()) { i = 6; j = 2; } // Reserve space for extra button if (handler) { ++j; } const QFont font = titleFont(option.font); const QFontMetrics fmTitle(font); const QString text = index.model()->data(index, Qt::DisplayRole).toString(); const QString comment = index.model()->data(index, KPluginModel::DescriptionRole).toString(); const int maxTextWidth = qMax(fmTitle.boundingRect(text).width(), option.fontMetrics.boundingRect(comment).width()); const auto iconSize = pluginSelector_d->listView->style()->pixelMetric(QStyle::PM_IconViewIconSize); return QSize(maxTextWidth + iconSize + s_margin * i + pushButton->sizeHint().width() * j, qMax(iconSize + s_margin * 2, fmTitle.height() + option.fontMetrics.height() + s_margin * 2)); } QList PluginDelegate::createItemWidgets(const QModelIndex &index) const { Q_UNUSED(index); QList widgetList; auto enabledCheckBox = new QCheckBox; connect(enabledCheckBox, &QAbstractButton::clicked, this, &PluginDelegate::slotStateChanged); auto aboutPushButton = new QPushButton; aboutPushButton->setIcon(QIcon::fromTheme(QStringLiteral("help-about-symbolic"))); aboutPushButton->setToolTip(i18n("About")); connect(aboutPushButton, &QAbstractButton::clicked, this, &PluginDelegate::slotAboutClicked); auto configurePushButton = new QPushButton; configurePushButton->setIcon(QIcon::fromTheme(QStringLiteral("configure-symbolic"))); configurePushButton->setToolTip(i18n("Configure")); connect(configurePushButton, &QAbstractButton::clicked, this, &PluginDelegate::slotConfigureClicked); const static QList blockedEvents{ QEvent::MouseButtonPress, QEvent::MouseButtonRelease, QEvent::MouseButtonDblClick, QEvent::KeyPress, QEvent::KeyRelease, }; setBlockedEventTypes(enabledCheckBox, blockedEvents); setBlockedEventTypes(aboutPushButton, blockedEvents); setBlockedEventTypes(configurePushButton, blockedEvents); widgetList << enabledCheckBox << aboutPushButton << configurePushButton; if (handler) { QPushButton *btn = handler(pluginSelector_d->pluginModel->data(index, KPluginModel::MetaDataRole).value()); if (btn) { widgetList << btn; } } return widgetList; } void PluginDelegate::updateItemWidgets(const QList &widgets, const QStyleOptionViewItem &option, const QPersistentModelIndex &index) const { int extraButtonWidth = 0; QPushButton *extraButton = nullptr; if (widgets.count() == 4) { extraButton = static_cast(widgets[3]); extraButtonWidth = extraButton->sizeHint().width() + s_margin; } auto checkBox = static_cast(widgets[0]); checkBox->resize(checkBox->sizeHint()); checkBox->move(pluginSelector_d->dependantLayoutValue(s_margin, checkBox->sizeHint().width(), option.rect.width()), option.rect.height() / 2 - checkBox->sizeHint().height() / 2); auto aboutPushButton = static_cast(widgets[1]); const QSize aboutPushButtonSizeHint = aboutPushButton->sizeHint(); aboutPushButton->resize(aboutPushButtonSizeHint); aboutPushButton->move(pluginSelector_d->dependantLayoutValue(option.rect.width() - s_margin - aboutPushButtonSizeHint.width() - extraButtonWidth, aboutPushButtonSizeHint.width(), option.rect.width()), option.rect.height() / 2 - aboutPushButtonSizeHint.height() / 2); auto configurePushButton = static_cast(widgets[2]); const QSize configurePushButtonSizeHint = configurePushButton->sizeHint(); configurePushButton->resize(configurePushButtonSizeHint); configurePushButton->move(pluginSelector_d->dependantLayoutValue(option.rect.width() - s_margin * 2 - configurePushButtonSizeHint.width() - aboutPushButtonSizeHint.width() - extraButtonWidth, configurePushButtonSizeHint.width(), option.rect.width()), option.rect.height() / 2 - configurePushButtonSizeHint.height() / 2); if (extraButton) { const QSize extraPushButtonSizeHint = extraButton->sizeHint(); extraButton->resize(extraPushButtonSizeHint); extraButton->move(pluginSelector_d->dependantLayoutValue(option.rect.width() - extraButtonWidth, extraPushButtonSizeHint.width(), option.rect.width()), option.rect.height() / 2 - extraPushButtonSizeHint.height() / 2); } if (!index.isValid() || !index.internalPointer()) { checkBox->setVisible(false); aboutPushButton->setVisible(false); configurePushButton->setVisible(false); if (extraButton) { extraButton->setVisible(false); } } else { const bool enabledByDefault = index.model()->data(index, KPluginModel::EnabledByDefaultRole).toBool(); const bool enabled = index.model()->data(index, KPluginModel::EnabledRole).toBool(); checkBox->setProperty("_kde_highlight_neutral", pluginSelector_d->showDefaultIndicator && enabledByDefault != enabled); checkBox->setChecked(index.model()->data(index, Qt::CheckStateRole).toBool()); checkBox->setEnabled(index.model()->data(index, KPluginModel::IsChangeableRole).toBool()); configurePushButton->setVisible(index.model()->data(index, KPluginModel::ConfigRole).value().isValid()); configurePushButton->setEnabled(index.model()->data(index, Qt::CheckStateRole).toBool()); } } void PluginDelegate::slotStateChanged(bool state) { if (!focusedIndex().isValid()) { return; } QModelIndex index = focusedIndex(); const_cast(index.model())->setData(index, state, Qt::CheckStateRole); } void PluginDelegate::slotAboutClicked() { const QModelIndex index = focusedIndex(); auto pluginMetaData = index.data(KPluginModel::MetaDataRole).value(); auto *aboutPlugin = new KAboutPluginDialog(pluginMetaData, itemView()); aboutPlugin->setAttribute(Qt::WA_DeleteOnClose); aboutPlugin->show(); } void PluginDelegate::slotConfigureClicked() { configure(focusedIndex()); } void PluginDelegate::configure(const QModelIndex &index) { const QAbstractItemModel *model = index.model(); const auto kcm = model->data(index, KPluginModel::ConfigRole).value(); auto configDialog = new QDialog(itemView()); configDialog->setAttribute(Qt::WA_DeleteOnClose); configDialog->setModal(true); configDialog->setWindowTitle(model->data(index, KPluginModel::NameRole).toString()); QWidget *kcmWrapper = new QWidget; auto kcmInstance = KCModuleLoader::loadModule(kcm, kcmWrapper, pluginSelector_d->kcmArguments); auto layout = new QVBoxLayout(configDialog); layout->addWidget(kcmWrapper); auto buttonBox = new QDialogButtonBox(configDialog); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::RestoreDefaults); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Ok), KStandardGuiItem::ok()); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel()); KGuiItem::assign(buttonBox->button(QDialogButtonBox::RestoreDefaults), KStandardGuiItem::defaults()); connect(buttonBox, &QDialogButtonBox::accepted, configDialog, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, configDialog, &QDialog::reject); connect(configDialog, &QDialog::accepted, this, [kcmInstance, this, model, index]() { Q_EMIT configCommitted(model->data(index, KPluginModel::IdRole).toString()); kcmInstance->save(); }); connect(configDialog, &QDialog::rejected, this, [kcmInstance]() { kcmInstance->load(); }); connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, [kcmInstance] { kcmInstance->defaults(); }); layout->addWidget(buttonBox); // Load KCM right before showing it kcmInstance->load(); configDialog->show(); } QFont PluginDelegate::titleFont(const QFont &baseFont) const { QFont retFont(baseFont); retFont.setBold(true); return retFont; } #include "moc_kpluginwidget.cpp" #include "moc_kpluginwidget_p.cpp"