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"