File indexing completed on 2024-05-19 05:00:22

0001 /*
0002     SPDX-FileCopyrightText: 2000 Yves Arrouye <yves@realnames.com>
0003     SPDX-FileCopyrightText: 2001, 2002 Dawit Alemayehu <adawit@kde.org>
0004     SPDX-FileCopyrightText: 2009 Nick Shaforostoff <shaforostoff@kde.ru>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "ikwsopts.h"
0010 #include "ikwsopts_p.h"
0011 
0012 #include "searchprovider.h"
0013 #include "searchproviderdlg.h"
0014 
0015 #include <KConfigGroup>
0016 #include <KLocalizedString>
0017 #include <KPluginFactory>
0018 #include <KSharedConfig>
0019 
0020 #include <QDBusConnection>
0021 #include <QDBusMessage>
0022 #include <QFile>
0023 #include <QPointer>
0024 #include <QSortFilterProxyModel>
0025 
0026 // BEGIN ProvidersModel
0027 
0028 ProvidersModel::~ProvidersModel()
0029 {
0030 }
0031 
0032 QVariant ProvidersModel::headerData(int section, Qt::Orientation orientation, int role) const
0033 {
0034     Q_UNUSED(orientation);
0035     if (role == Qt::DisplayRole) {
0036         switch (section) {
0037         case Name:
0038             return i18nc("@title:column Name label from web search keyword column", "Name");
0039         case Shortcuts:
0040             return i18nc("@title:column", "Keywords");
0041         case Preferred:
0042             return i18nc("@title:column", "Preferred");
0043         default:
0044             break;
0045         }
0046     }
0047     return QVariant();
0048 }
0049 
0050 Qt::ItemFlags ProvidersModel::flags(const QModelIndex &index) const
0051 {
0052     if (!index.isValid()) {
0053         return Qt::ItemIsEnabled;
0054     }
0055     if (index.column() == Preferred) {
0056         return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
0057     }
0058     return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
0059 }
0060 
0061 bool ProvidersModel::setData(const QModelIndex &index, const QVariant &value, int role)
0062 {
0063     if (role == Qt::CheckStateRole) {
0064         if (value.toInt() == Qt::Checked) {
0065             m_favoriteEngines.insert(m_providers.at(index.row())->desktopEntryName());
0066         } else {
0067             m_favoriteEngines.remove(m_providers.at(index.row())->desktopEntryName());
0068         }
0069         Q_EMIT dataModified();
0070         return true;
0071     }
0072     return false;
0073 }
0074 
0075 QVariant ProvidersModel::data(const QModelIndex &index, int role) const
0076 {
0077     if (!index.isValid()) {
0078         return QVariant();
0079     }
0080 
0081     if (role == Qt::CheckStateRole && index.column() == Preferred) {
0082         return m_favoriteEngines.contains(m_providers.at(index.row())->desktopEntryName()) ? Qt::Checked : Qt::Unchecked;
0083     }
0084 
0085     if (role == Qt::DecorationRole && index.column() == Name) {
0086         return QIcon::fromTheme(m_providers.at(index.row())->iconName());
0087     }
0088 
0089     if (role == Qt::DisplayRole) {
0090         if (index.column() == Name) {
0091             return m_providers.at(index.row())->name();
0092         }
0093         if (index.column() == Shortcuts) {
0094             return m_providers.at(index.row())->keys().join(QLatin1Char(','));
0095         }
0096     }
0097 
0098     if (role == Qt::ToolTipRole || role == Qt::WhatsThisRole) {
0099         if (index.column() == Preferred) {
0100             return xi18nc("@info:tooltip",
0101                           "Check this box to select the highlighted web search keyword "
0102                           "as preferred.<nl/>Preferred web search keywords are used in "
0103                           "places where only a few select keywords can be shown "
0104                           "at one time.");
0105         }
0106     }
0107 
0108     if (role == Qt::UserRole) {
0109         return index.row(); // a nice way to bypass proxymodel
0110     }
0111 
0112     return QVariant();
0113 }
0114 
0115 void ProvidersModel::setProviders(const QList<SearchProvider *> &providers, const QStringList &favoriteEngines)
0116 {
0117     m_providers = providers;
0118     setFavoriteProviders(favoriteEngines);
0119 }
0120 
0121 void ProvidersModel::setFavoriteProviders(const QStringList &favoriteEngines)
0122 {
0123     beginResetModel();
0124 
0125     m_favoriteEngines = QSet<QString>(favoriteEngines.begin(), favoriteEngines.end());
0126 
0127     endResetModel();
0128 }
0129 
0130 int ProvidersModel::rowCount(const QModelIndex &parent) const
0131 {
0132     if (parent.isValid()) {
0133         return 0;
0134     }
0135     return m_providers.size();
0136 }
0137 
0138 QAbstractListModel *ProvidersModel::createListModel()
0139 {
0140     ProvidersListModel *pListModel = new ProvidersListModel(m_providers, this);
0141     connect(this, &QAbstractItemModel::modelAboutToBeReset, pListModel, &QAbstractItemModel::modelAboutToBeReset);
0142     connect(this, &QAbstractItemModel::modelReset, pListModel, &QAbstractItemModel::modelReset);
0143     connect(this, &QAbstractItemModel::dataChanged, pListModel, &ProvidersListModel::emitDataChanged);
0144     connect(this, &QAbstractItemModel::rowsAboutToBeInserted, pListModel, &ProvidersListModel::emitRowsAboutToBeInserted);
0145     connect(this, &QAbstractItemModel::rowsAboutToBeRemoved, pListModel, &ProvidersListModel::emitRowsAboutToBeRemoved);
0146     connect(this, &QAbstractItemModel::rowsInserted, pListModel, &ProvidersListModel::emitRowsInserted);
0147     connect(this, &QAbstractItemModel::rowsRemoved, pListModel, &ProvidersListModel::emitRowsRemoved);
0148 
0149     return pListModel;
0150 }
0151 
0152 void ProvidersModel::deleteProvider(SearchProvider *p)
0153 {
0154     const int row = m_providers.indexOf(p);
0155     beginRemoveRows(QModelIndex(), row, row);
0156     m_favoriteEngines.remove(m_providers.takeAt(row)->desktopEntryName());
0157     endRemoveRows();
0158     Q_EMIT dataModified();
0159 }
0160 
0161 void ProvidersModel::addProvider(SearchProvider *p)
0162 {
0163     beginInsertRows(QModelIndex(), m_providers.size(), m_providers.size());
0164     m_providers.append(p);
0165     endInsertRows();
0166     Q_EMIT dataModified();
0167 }
0168 
0169 void ProvidersModel::changeProvider(SearchProvider *p)
0170 {
0171     const int row = m_providers.indexOf(p);
0172     Q_EMIT dataChanged(index(row, 0), index(row, ColumnCount - 1));
0173     Q_EMIT dataModified();
0174 }
0175 
0176 QStringList ProvidersModel::favoriteEngines() const
0177 {
0178     return QStringList(m_favoriteEngines.cbegin(), m_favoriteEngines.cend());
0179 }
0180 
0181 // END ProvidersModel
0182 
0183 // BEGIN ProvidersListModel
0184 ProvidersListModel::ProvidersListModel(QList<SearchProvider *> &providers, QObject *parent)
0185     : QAbstractListModel(parent)
0186     , m_providers(providers)
0187 {
0188 }
0189 
0190 QVariant ProvidersListModel::data(const QModelIndex &index, int role) const
0191 {
0192     if (!index.isValid()) {
0193         return QVariant();
0194     }
0195 
0196     const int row = index.row();
0197     const bool noProvider = (row == m_providers.size()); // `None` is the last item
0198 
0199     switch (role) {
0200     case Qt::DisplayRole:
0201         if (noProvider) {
0202             return i18nc("@item:inlistbox No default web search keyword", "None");
0203         }
0204         return m_providers.at(index.row())->name();
0205     case ShortNameRole:
0206         if (noProvider) {
0207             return QString();
0208         }
0209         return m_providers.at(index.row())->desktopEntryName();
0210     case Qt::DecorationRole:
0211         if (noProvider) {
0212             return QIcon::fromTheme(QStringLiteral("empty"));
0213         }
0214         return QIcon::fromTheme(m_providers.at(index.row())->iconName());
0215     }
0216 
0217     return QVariant();
0218 }
0219 
0220 int ProvidersListModel::rowCount(const QModelIndex &parent) const
0221 {
0222     if (parent.isValid()) {
0223         return 0;
0224     }
0225     return m_providers.size() + 1;
0226 }
0227 
0228 // END ProvidersListModel
0229 
0230 static QSortFilterProxyModel *wrapInProxyModel(QAbstractItemModel *model)
0231 {
0232     QSortFilterProxyModel *proxyModel = new QSortFilterProxyModel(model);
0233     proxyModel->setSourceModel(model);
0234     proxyModel->setDynamicSortFilter(true);
0235     proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
0236     proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
0237     proxyModel->setFilterKeyColumn(-1);
0238     return proxyModel;
0239 }
0240 
0241 FilterOptions::FilterOptions(QObject *parent, const KPluginMetaData &data)
0242     : KCModule(parent, data)
0243     , m_providersModel(new ProvidersModel(this))
0244 {
0245     // Used for tab text in the KCM
0246     widget()->setWindowTitle(i18n("Search F&ilters"));
0247 
0248     m_dlg.setupUi(widget());
0249 
0250     QSortFilterProxyModel *searchProviderModel = wrapInProxyModel(m_providersModel);
0251     m_dlg.lvSearchProviders->setModel(searchProviderModel);
0252     m_dlg.cmbDefaultEngine->setModel(wrapInProxyModel(m_providersModel->createListModel()));
0253 
0254     // Connect all the signals/slots...
0255     connect(m_dlg.cbEnableShortcuts, &QAbstractButton::toggled, this, &FilterOptions::markAsChanged);
0256     connect(m_dlg.cbEnableShortcuts, &QAbstractButton::toggled, this, &FilterOptions::updateSearchProviderEditingButons);
0257     connect(m_dlg.cbUseSelectedShortcutsOnly, &QAbstractButton::toggled, this, &FilterOptions::markAsChanged);
0258 
0259     connect(m_providersModel, &ProvidersModel::dataModified, this, &FilterOptions::markAsChanged);
0260     connect(m_dlg.cmbDefaultEngine, qOverload<int>(&QComboBox::currentIndexChanged), this, &FilterOptions::markAsChanged);
0261     connect(m_dlg.cmbDelimiter, qOverload<int>(&QComboBox::currentIndexChanged), this, &FilterOptions::markAsChanged);
0262 
0263     connect(m_dlg.pbNew, &QAbstractButton::clicked, this, &FilterOptions::addSearchProvider);
0264     connect(m_dlg.pbDelete, &QAbstractButton::clicked, this, &FilterOptions::deleteSearchProvider);
0265     connect(m_dlg.pbChange, &QAbstractButton::clicked, this, &FilterOptions::changeSearchProvider);
0266     connect(m_dlg.lvSearchProviders->selectionModel(), &QItemSelectionModel::currentChanged, this, &FilterOptions::updateSearchProviderEditingButons);
0267     connect(m_dlg.lvSearchProviders, &QAbstractItemView::doubleClicked, this, &FilterOptions::changeSearchProvider);
0268     connect(m_dlg.searchLineEdit, &QLineEdit::textEdited, searchProviderModel, &QSortFilterProxyModel::setFilterFixedString);
0269 }
0270 
0271 void FilterOptions::setDefaultEngine(int index)
0272 {
0273     QSortFilterProxyModel *proxy = qobject_cast<QSortFilterProxyModel *>(m_dlg.cmbDefaultEngine->model());
0274     if (index == -1) {
0275         index = proxy->rowCount() - 1; //"None" is the last
0276     }
0277     const QModelIndex modelIndex = proxy->mapFromSource(proxy->sourceModel()->index(index, 0));
0278     m_dlg.cmbDefaultEngine->setCurrentIndex(modelIndex.row());
0279     m_dlg.cmbDefaultEngine->view()->setCurrentIndex(modelIndex); // TODO: remove this when Qt bug is fixed
0280 }
0281 
0282 void FilterOptions::load()
0283 {
0284     KConfig config(QStringLiteral("kuriikwsfilterrc"), KConfig::NoGlobals);
0285     KConfigGroup group = config.group(QStringLiteral("General"));
0286 
0287     const QString defaultSearchEngine = group.readEntry("DefaultWebShortcut", "duckduckgo");
0288     const QStringList favoriteEngines = group.readEntry("PreferredWebShortcuts", m_defaultProviders);
0289 
0290     const QList<SearchProvider *> allProviders = m_registry.findAll();
0291     QList<SearchProvider *> providers;
0292     for (auto *provider : allProviders) {
0293         if (!provider->isHidden()) {
0294             providers << provider;
0295         }
0296     }
0297 
0298     int defaultProviderIndex = providers.size(); // default is "None", it is last in the list
0299 
0300     for (SearchProvider *provider : std::as_const(providers)) {
0301         if (defaultSearchEngine == provider->desktopEntryName()) {
0302             defaultProviderIndex = providers.indexOf(provider);
0303             break;
0304         }
0305     }
0306 
0307     m_providersModel->setProviders(providers, favoriteEngines);
0308     m_dlg.lvSearchProviders->setColumnWidth(0, 200);
0309     m_dlg.lvSearchProviders->resizeColumnToContents(1);
0310     m_dlg.lvSearchProviders->sortByColumn(0, Qt::AscendingOrder);
0311     m_dlg.cmbDefaultEngine->model()->sort(0, Qt::AscendingOrder);
0312     setDefaultEngine(defaultProviderIndex);
0313 
0314     m_dlg.cbEnableShortcuts->setChecked(group.readEntry("EnableWebShortcuts", true));
0315     m_dlg.cbUseSelectedShortcutsOnly->setChecked(group.readEntry("UsePreferredWebShortcutsOnly", false));
0316 
0317     const QString delimiter = group.readEntry("KeywordDelimiter", ":");
0318     setDelimiter(delimiter.at(0).toLatin1());
0319 }
0320 
0321 char FilterOptions::delimiter()
0322 {
0323     const char delimiters[] = {':', ' '};
0324     return delimiters[m_dlg.cmbDelimiter->currentIndex()];
0325 }
0326 
0327 void FilterOptions::setDelimiter(char sep)
0328 {
0329     m_dlg.cmbDelimiter->setCurrentIndex(sep == ' ');
0330 }
0331 
0332 void FilterOptions::save()
0333 {
0334     KConfig config(QStringLiteral("kuriikwsfilterrc"), KConfig::NoGlobals);
0335 
0336     KConfigGroup group = config.group(QStringLiteral("General"));
0337     group.writeEntry("EnableWebShortcuts", m_dlg.cbEnableShortcuts->isChecked());
0338     group.writeEntry("KeywordDelimiter", QString(QLatin1Char(delimiter())));
0339     group.writeEntry("DefaultWebShortcut", m_dlg.cmbDefaultEngine->view()->currentIndex().data(ProvidersListModel::ShortNameRole));
0340     group.writeEntry("PreferredWebShortcuts", m_providersModel->favoriteEngines());
0341     group.writeEntry("UsePreferredWebShortcutsOnly", m_dlg.cbUseSelectedShortcutsOnly->isChecked());
0342 
0343     const QList<SearchProvider *> providers = m_providersModel->providers();
0344     const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kf6/searchproviders/");
0345 
0346     for (SearchProvider *provider : providers) {
0347         if (!provider->isDirty()) {
0348             continue;
0349         }
0350 
0351         KConfig _service(path + provider->desktopEntryName() + QLatin1String(".desktop"), KConfig::SimpleConfig);
0352         KConfigGroup service(&_service, QStringLiteral("Desktop Entry"));
0353         service.writeEntry("Type", "Service");
0354         service.writeEntry("Name", provider->name());
0355         service.writeEntry("Query", provider->query());
0356         service.writeEntry("Keys", provider->keys());
0357         service.writeEntry("Charset", provider->charset());
0358         service.writeEntry("Hidden", false); // we might be overwriting a hidden entry
0359     }
0360 
0361     const QStringList servicesDirs =
0362         QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kf6/searchproviders/"), QStandardPaths::LocateDirectory);
0363     for (const QString &providerName : std::as_const(m_deletedProviders)) {
0364         QStringList matches;
0365         for (const QString &dir : servicesDirs) {
0366             QString current = dir + QLatin1Char('/') + providerName + QLatin1String(".desktop");
0367             if (QFile::exists(current)) {
0368                 matches += current;
0369             }
0370         }
0371 
0372         // Shouldn't happen
0373         if (matches.isEmpty()) {
0374             continue;
0375         }
0376 
0377         if (matches.size() == 1 && matches.first().startsWith(path)) {
0378             // If only the local copy existed, unlink it
0379             // TODO: error handling
0380             QFile::remove(matches.first());
0381             continue;
0382         }
0383 
0384         KConfig _service(path + providerName + QLatin1String(".desktop"), KConfig::SimpleConfig);
0385         KConfigGroup service(&_service, QStringLiteral("Desktop Entry"));
0386         service.writeEntry("Type", "Service");
0387         service.writeEntry("Hidden", true);
0388     }
0389 
0390     config.sync();
0391 
0392     setNeedsSave(false);
0393 
0394     // Update filters in running applications...
0395     QDBusMessage msg = QDBusMessage::createSignal(QStringLiteral("/"), QStringLiteral("org.kde.KUriFilterPlugin"), QStringLiteral("configure"));
0396     QDBusConnection::sessionBus().send(msg);
0397 }
0398 
0399 void FilterOptions::defaults()
0400 {
0401     m_dlg.cbEnableShortcuts->setChecked(true);
0402     m_dlg.cbUseSelectedShortcutsOnly->setChecked(false);
0403     m_providersModel->setFavoriteProviders(m_defaultProviders);
0404     setDelimiter(':');
0405 
0406     const QList<SearchProvider *> providers = m_providersModel->providers();
0407     int defaultProviderIndex = providers.size(); // default is "None", it is last in the list
0408     for (SearchProvider *provider : std::as_const(providers)) {
0409         if (QLatin1String("duckduckgo") == provider->desktopEntryName()) {
0410             defaultProviderIndex = providers.indexOf(provider);
0411             break;
0412         }
0413     }
0414     setDefaultEngine(defaultProviderIndex);
0415 }
0416 
0417 void FilterOptions::addSearchProvider()
0418 {
0419     QList<SearchProvider *> providers = m_providersModel->providers();
0420     QPointer<SearchProviderDialog> dlg = new SearchProviderDialog(nullptr, providers, widget());
0421 
0422     if (dlg->exec()) {
0423         m_providersModel->addProvider(dlg->provider());
0424         m_providersModel->changeProvider(dlg->provider());
0425     }
0426     delete dlg;
0427 }
0428 
0429 void FilterOptions::changeSearchProvider()
0430 {
0431     QList<SearchProvider *> providers = m_providersModel->providers();
0432     SearchProvider *provider = providers.at(m_dlg.lvSearchProviders->currentIndex().data(Qt::UserRole).toInt());
0433     QPointer<SearchProviderDialog> dlg = new SearchProviderDialog(provider, providers, widget());
0434 
0435     if (dlg->exec()) {
0436         m_providersModel->changeProvider(dlg->provider());
0437     }
0438 
0439     delete dlg;
0440 }
0441 
0442 void FilterOptions::deleteSearchProvider()
0443 {
0444     SearchProvider *provider = m_providersModel->providers().at(m_dlg.lvSearchProviders->currentIndex().data(Qt::UserRole).toInt());
0445     m_deletedProviders.append(provider->desktopEntryName());
0446     m_providersModel->deleteProvider(provider);
0447 }
0448 
0449 void FilterOptions::updateSearchProviderEditingButons()
0450 {
0451     const bool enable = (m_dlg.cbEnableShortcuts->isChecked() && m_dlg.lvSearchProviders->currentIndex().isValid());
0452     m_dlg.pbChange->setEnabled(enable);
0453     m_dlg.pbDelete->setEnabled(enable);
0454 }
0455 
0456 K_PLUGIN_CLASS_WITH_JSON(FilterOptions, "kcm_webshortcuts.json")
0457 
0458 #include "ikwsopts.moc"