File indexing completed on 2024-05-12 11:54:36

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