1 /* This file is part of the KDE project
2 SPDX-FileCopyrightText: 2004 Arend van Beelen jr. <arend@auton.nl>
3 SPDX-FileCopyrightText: 2009 Fredy Yanardi <fyanardi@gmail.com>
4
5 SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8 #include "searchbar.h"
9
10 #include "OpenSearchManager.h"
11 #include "WebShortcutWidget.h"
12
13 #include <KBuildSycocaProgressDialog>
14 #include <KCompletionBox>
15 #include <KConfigGroup>
16 #include <KSharedConfig>
17 #include <KDesktopFile>
18 #include <KDialogJobUiDelegate>
19 #include <KPluginFactory>
20 #include <KActionCollection>
21 #include <KIO/CommandLauncherJob>
22 #include <KMainWindow>
23 #include <KParts/Part>
24 #include <KParts/BrowserExtension>
25 #include <KParts/TextExtension>
26 #include <KParts/HtmlExtension>
27 #include <KParts/SelectorInterface>
28 #include <KParts/PartActivateEvent>
29 #include <KLocalizedString>
30
31 #include <QLineEdit>
32 #include <QApplication>
33 #include <QDir>
34 #include <QTimer>
35 #include <QMenu>
36 #include <QStyle>
37 #include <QPainter>
38 #include <QMouseEvent>
39 #include <QDBusConnection>
40 #include <QDBusMessage>
41 #include <QWidgetAction>
42 #include <QStandardPaths>
43
44 #include "searchbar_debug.h"
45
K_PLUGIN_FACTORY(SearchBarPluginFactory,registerPlugin<SearchBarPlugin> ();)46 K_PLUGIN_FACTORY(SearchBarPluginFactory, registerPlugin<SearchBarPlugin>();)
47
48 SearchBarPlugin::SearchBarPlugin(QObject *parent,
49 const QVariantList &) :
50 KParts::Plugin(parent),
51 m_popupMenu(nullptr),
52 m_addWSWidget(nullptr),
53 m_searchMode(UseSearchProvider),
54 m_urlEnterLock(false),
55 m_openSearchManager(new OpenSearchManager(this)),
56 m_reloadConfiguration(false)
57 {
58 m_searchCombo = new SearchBarCombo(nullptr);
59 m_searchCombo->lineEdit()->installEventFilter(this);
60 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
__anoncac8a1260102(int n)61 connect(m_searchCombo, QOverload<int>::of(&QComboBox::activated), this, [this](int n){startSearch(m_searchCombo->itemText(n));});
62 #else
63 connect(m_searchCombo, &QComboBox::textActivated, this, &SearchBarPlugin::startSearch);
64 #endif
65 connect(m_searchCombo, &SearchBarCombo::iconClicked, this, &SearchBarPlugin::showSelectionMenu);
66 m_searchCombo->setWhatsThis(i18n("Search Bar<p>"
67 "Enter a search term. Click on the icon to change search mode or provider.</p>"));
68 connect(m_searchCombo, &SearchBarCombo::suggestionEnabled, this, &SearchBarPlugin::enableSuggestion);
69
70 m_searchComboAction = new QWidgetAction(actionCollection());
71 actionCollection()->addAction(QStringLiteral("toolbar_search_bar"), m_searchComboAction);
72 m_searchComboAction->setText(i18n("Search Bar"));
73 m_searchComboAction->setDefaultWidget(m_searchCombo);
74 actionCollection()->setShortcutsConfigurable(m_searchComboAction, false);
75
76 QAction *a = actionCollection()->addAction(QStringLiteral("focus_search_bar"));
77 a->setText(i18n("Focus Searchbar"));
78 actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_S));
79 connect(a, &QAction::triggered, this, &SearchBarPlugin::focusSearchbar);
80 m_searchProvidersDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/kde5/services/searchproviders/";
81 QDir().mkpath(m_searchProvidersDir);
82 configurationChanged();
83
84 m_timer = new QTimer(this);
85 m_timer->setSingleShot(true);
86 connect(m_timer, &QTimer::timeout, this, &SearchBarPlugin::requestSuggestion);
87
88 // parent is the KonqMainWindow and we want to listen to PartActivateEvent events.
89 parent->installEventFilter(this);
90
91 connect(m_searchCombo->lineEdit(), &QLineEdit::textEdited,
92 this, &SearchBarPlugin::searchTextChanged);
93 connect(m_openSearchManager, &OpenSearchManager::suggestionReceived,
94 this, &SearchBarPlugin::addSearchSuggestion);
95 connect(m_openSearchManager, &OpenSearchManager::openSearchEngineAdded,
96 this, &SearchBarPlugin::openSearchEngineAdded);
97
98 QDBusConnection::sessionBus().connect(QString(), QString(), QStringLiteral("org.kde.KUriFilterPlugin"),
99 QStringLiteral("configure"), this, SLOT(reloadConfiguration()));
100 }
101
~SearchBarPlugin()102 SearchBarPlugin::~SearchBarPlugin()
103 {
104 KConfigGroup config(KSharedConfig::openConfig(), "SearchBar");
105 config.writeEntry("Mode", (int) m_searchMode);
106 config.writeEntry("CurrentEngine", m_currentEngine);
107 config.writeEntry("SuggestionEnabled", m_suggestionEnabled);
108
109 delete m_searchCombo;
110 m_searchCombo = nullptr;
111 }
112
eventFilter(QObject * o,QEvent * e)113 bool SearchBarPlugin::eventFilter(QObject *o, QEvent *e)
114 {
115 if (qobject_cast<KMainWindow *>(o) && KParts::PartActivateEvent::test(e)) {
116 KParts::PartActivateEvent *partEvent = static_cast<KParts::PartActivateEvent *>(e);
117 KParts::ReadOnlyPart *part = qobject_cast<KParts::ReadOnlyPart *>(partEvent->part());
118 //qCDebug(SEARCHBAR_LOG) << "Embedded part changed to " << part;
119 if (part && (m_part.isNull() || part != m_part)) {
120 m_part = part;
121
122 // Delete the popup menu so a new one can be created with the
123 // appropriate entries the next time it is shown...
124 // ######## TODO: This loses the opensearch entries for the old part!!!
125 if (m_popupMenu) {
126 delete m_popupMenu;
127 m_popupMenu = nullptr;
128 m_addSearchActions.clear(); // the actions had the menu as parent, so they're deleted now
129 }
130
131 // Change the search mode if it is set to FindInThisPage since
132 // that feature is currently KHTML specific. It is also completely
133 // redundant and unnecessary.
134 if (m_searchMode == FindInThisPage && enableFindInPage()) {
135 nextSearchEntry();
136 }
137
138 connect(part, QOverload<>::of(&KParts::ReadOnlyPart::completed), this, &SearchBarPlugin::HTMLDocLoaded);
139 connect(part, &KParts::ReadOnlyPart::started, this, &SearchBarPlugin::HTMLLoadingStarted);
140 }
141 // Delay since when destroying tabs part 0 gets activated for a bit, before the proper part
142 QTimer::singleShot(0, this, &SearchBarPlugin::updateComboVisibility);
143 } else if (o == m_searchCombo->lineEdit() && e->type() == QEvent::KeyPress) {
144 QKeyEvent *k = (QKeyEvent *)e;
145 if (k->modifiers() & Qt::ControlModifier) {
146 if (k->key() == Qt::Key_Down) {
147 nextSearchEntry();
148 return true;
149 }
150 if (k->key() == Qt::Key_Up) {
151 previousSearchEntry();
152 return true;
153 }
154 }
155 }
156 return KParts::Plugin::eventFilter(o, e);
157 }
158
nextSearchEntry()159 void SearchBarPlugin::nextSearchEntry()
160 {
161 if (m_searchMode == FindInThisPage) {
162 m_searchMode = UseSearchProvider;
163 if (m_searchEngines.isEmpty()) {
164 m_currentEngine = QStringLiteral("google");
165 } else {
166 m_currentEngine = m_searchEngines.first();
167 }
168 } else {
169 const int index = m_searchEngines.indexOf(m_currentEngine) + 1;
170 if (index >= m_searchEngines.count()) {
171 m_searchMode = FindInThisPage;
172 } else {
173 m_currentEngine = m_searchEngines.at(index);
174 }
175 }
176 setIcon();
177 }
178
previousSearchEntry()179 void SearchBarPlugin::previousSearchEntry()
180 {
181 if (m_searchMode == FindInThisPage) {
182 m_searchMode = UseSearchProvider;
183 if (m_searchEngines.isEmpty()) {
184 m_currentEngine = QStringLiteral("google");
185 } else {
186 m_currentEngine = m_searchEngines.last();
187 }
188 } else {
189 const int index = m_searchEngines.indexOf(m_currentEngine) - 1;
190 if (index <= 0) {
191 m_searchMode = FindInThisPage;
192 } else {
193 m_currentEngine = m_searchEngines.at(index);
194 }
195 }
196 setIcon();
197 }
198
199 // Called when activating the combobox (Key_Return, or item in popup or in completionbox)
startSearch(const QString & search)200 void SearchBarPlugin::startSearch(const QString &search)
201 {
202 if (m_urlEnterLock || search.isEmpty() || m_part.isNull()) {
203 return;
204 }
205 m_timer->stop();
206 m_lastSearch = search;
207
208 if (m_searchMode == FindInThisPage) {
209 KParts::TextExtension *textExt = KParts::TextExtension::childObject(m_part);
210 if (textExt) {
211 textExt->findText(search, KFind::SearchOptions());
212 }
213 } else if (m_searchMode == UseSearchProvider) {
214 m_urlEnterLock = true;
215 const KUriFilterSearchProvider &provider = m_searchProviders.value(m_currentEngine);
216 KUriFilterData data;
217 data.setData(provider.defaultKey() + m_delimiter + search);
218 //qCDebug(SEARCHBAR_LOG) << "Query:" << (provider.defaultKey() + m_delimiter + search);
219 if (!KUriFilter::self()->filterSearchUri(data, KUriFilter::WebShortcutFilter)) {
220 qCWarning(SEARCHBAR_LOG) << "Failed to filter using web shortcut:" << provider.defaultKey();
221 return;
222 }
223
224 KParts::BrowserExtension *ext = KParts::BrowserExtension::childObject(m_part);
225 if (QApplication::keyboardModifiers() & Qt::ControlModifier) {
226 KParts::OpenUrlArguments arguments;
227 KParts::BrowserArguments browserArguments;
228 browserArguments.setNewTab(true);
229 if (ext) {
230 emit ext->createNewWindow(data.uri(), arguments, browserArguments);
231 }
232 } else {
233 if (ext) {
234 emit ext->openUrlRequest(data.uri());
235 if (!m_part.isNull()) {
236 m_part->widget()->setFocus(); // #152923
237 }
238 }
239 }
240 }
241
242 m_searchCombo->addToHistory(search);
243 m_searchCombo->setItemIcon(0, m_searchIcon);
244
245 m_urlEnterLock = false;
246 }
247
setIcon()248 void SearchBarPlugin::setIcon()
249 {
250 if (m_searchMode == FindInThisPage) {
251 m_searchIcon = QIcon::fromTheme(QStringLiteral("edit-find")).pixmap(qApp->style()->pixelMetric(QStyle::PM_SmallIconSize));
252 } else {
253 const QString engine = (m_currentEngine.isEmpty() ? m_searchEngines.first() : m_currentEngine);
254 //qCDebug(SEARCHBAR_LOG) << "Icon Name:" << m_searchProviders.value(engine).iconName();
255 const QString iconName = m_searchProviders.value(engine).iconName();
256 if (iconName.startsWith(QLatin1Char('/'))) {
257 m_searchIcon = QPixmap(iconName);
258 } else {
259 m_searchIcon = QIcon::fromTheme(iconName).pixmap(qApp->style()->pixelMetric(QStyle::PM_SmallIconSize));
260 }
261 }
262
263 // Create a bit wider icon with arrow
264 QPixmap arrowmap = QPixmap(m_searchIcon.width() + 5, m_searchIcon.height() + 5);
265 arrowmap.fill(m_searchCombo->lineEdit()->palette().color(m_searchCombo->lineEdit()->backgroundRole()));
266 QPainter p(&arrowmap);
267 p.drawPixmap(0, 2, m_searchIcon);
268 QStyleOption opt;
269 opt.state = QStyle::State_None;
270 opt.rect = QRect(arrowmap.width() - 6, arrowmap.height() - 5, 6, 5);
271 m_searchCombo->style()->drawPrimitive(QStyle::PE_IndicatorArrowDown, &opt, &p, m_searchCombo);
272 p.end();
273 m_searchIcon = arrowmap;
274 m_searchCombo->setIcon(m_searchIcon);
275
276 // Set the placeholder text to be the search engine name...
277 if (m_searchProviders.contains(m_currentEngine)) {
278 m_searchCombo->lineEdit()->setPlaceholderText(m_searchProviders.value(m_currentEngine).name());
279 }
280 }
281
showSelectionMenu()282 void SearchBarPlugin::showSelectionMenu()
283 {
284 // Update the configuration, if needed, before showing the menu items...
285 if (m_reloadConfiguration) {
286 configurationChanged();
287 }
288
289 if (!m_popupMenu) {
290 m_popupMenu = new QMenu(m_searchCombo);
291 m_popupMenu->setObjectName(QStringLiteral("search selection menu"));
292
293 if (enableFindInPage()) {
294 m_popupMenu->addAction(QIcon::fromTheme(QStringLiteral("edit-find")), i18n("Find in This Page"),
295 this, &SearchBarPlugin::useFindInThisPage);
296 m_popupMenu->addSeparator();
297 }
298
299 for (int i = 0, count = m_searchEngines.count(); i != count; ++i) {
300 const KUriFilterSearchProvider &provider = m_searchProviders.value(m_searchEngines.at(i));
301 QAction *action = m_popupMenu->addAction(QIcon::fromTheme(provider.iconName()), provider.name());
302 action->setData(QVariant::fromValue(i));
303 }
304
305 m_popupMenu->addSeparator();
306 m_popupMenu->addAction(QIcon::fromTheme(QStringLiteral("preferences-web-browser-shortcuts")), i18n("Select Search Engines..."),
307 this, &SearchBarPlugin::selectSearchEngines);
308 connect(m_popupMenu, &QMenu::triggered, this, &SearchBarPlugin::menuActionTriggered);
309 } else {
310 Q_FOREACH (QAction *action, m_addSearchActions) {
311 m_popupMenu->removeAction(action);
312 delete action;
313 }
314 m_addSearchActions.clear();
315 }
316
317 QList<QAction *> actions = m_popupMenu->actions();
318 QAction *before = nullptr;
319 if (actions.size() > 1) {
320 before = actions[actions.size() - 2];
321 }
322
323 Q_FOREACH (const QString &title, m_openSearchDescs.keys()) {
324 QAction *addSearchAction = new QAction(m_popupMenu);
325 addSearchAction->setText(i18n("Add %1...", title));
326 m_addSearchActions.append(addSearchAction);
327 addSearchAction->setData(QVariant::fromValue(title));
328 m_popupMenu->insertAction(before, addSearchAction);
329 }
330
331 m_popupMenu->popup(m_searchCombo->mapToGlobal(QPoint(0, m_searchCombo->height() + 1)));
332 }
333
useFindInThisPage()334 void SearchBarPlugin::useFindInThisPage()
335 {
336 m_searchMode = FindInThisPage;
337 setIcon();
338 }
339
menuActionTriggered(QAction * action)340 void SearchBarPlugin::menuActionTriggered(QAction *action)
341 {
342 bool ok = false;
343 const int id = action->data().toInt(&ok);
344 if (ok) {
345 m_searchMode = UseSearchProvider;
346 m_currentEngine = m_searchEngines.at(id);
347 setIcon();
348 m_openSearchManager->setSearchProvider(m_currentEngine);
349 m_searchCombo->lineEdit()->selectAll();
350 return;
351 }
352
353 m_searchCombo->lineEdit()->setPlaceholderText(QString());
354 const QString openSearchTitle = action->data().toString();
355 if (!openSearchTitle.isEmpty()) {
356 const QString openSearchHref = m_openSearchDescs.value(openSearchTitle);
357 QUrl url;
358 QUrl openSearchUrl = QUrl(openSearchHref);
359 if (openSearchUrl.isRelative()) {
360 const QUrl docUrl = !m_part.isNull() ? m_part->url() : QUrl();
361 QString host = docUrl.scheme() + QLatin1String("://") + docUrl.host();
362 if (docUrl.port() != -1) {
363 host += QLatin1String(":") + QString::number(docUrl.port());
364 }
365 url = docUrl.resolved(QUrl(openSearchHref));
366 } else {
367 url = QUrl(openSearchHref);
368 }
369 //qCDebug(SEARCHBAR_LOG) << "Adding open search Engine: " << openSearchTitle << " : " << openSearchHref;
370 m_openSearchManager->addOpenSearchEngine(url, openSearchTitle);
371 }
372 }
373
selectSearchEngines()374 void SearchBarPlugin::selectSearchEngines()
375 {
376 KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(QStringLiteral("kcmshell5 webshortcuts"));
377 job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, !m_part.isNull() ? m_part->widget() : nullptr));
378 job->start();
379 }
380
configurationChanged()381 void SearchBarPlugin::configurationChanged()
382 {
383 delete m_popupMenu;
384 m_popupMenu = nullptr;
385 m_addSearchActions.clear();
386 m_searchEngines.clear();
387 m_searchProviders.clear();
388
389 KUriFilterData data;
390 data.setSearchFilteringOptions(KUriFilterData::RetrievePreferredSearchProvidersOnly);
391 data.setAlternateDefaultSearchProvider(QStringLiteral("google"));
392
393 if (KUriFilter::self()->filterSearchUri(data, KUriFilter::NormalTextFilter)) {
394 m_delimiter = data.searchTermSeparator();
395 Q_FOREACH (const QString &engine, data.preferredSearchProviders()) {
396 //qCDebug(SEARCHBAR_LOG) << "Found search provider:" << engine;
397 const KUriFilterSearchProvider &provider = data.queryForSearchProvider(engine);
398
399 m_searchProviders.insert(provider.desktopEntryName(), provider);
400 m_searchEngines << provider.desktopEntryName();
401 }
402 }
403
404 //qCDebug(SEARCHBAR_LOG) << "Found search engines:" << m_searchEngines;
405 KConfigGroup config = KConfigGroup(KSharedConfig::openConfig(), "SearchBar");
406 m_searchMode = (SearchModes) config.readEntry("Mode", static_cast<int>(UseSearchProvider));
407 const QString defaultSearchEngine((m_searchEngines.isEmpty() ? QStringLiteral("google") : m_searchEngines.first()));
408 m_currentEngine = config.readEntry("CurrentEngine", defaultSearchEngine);
409 m_suggestionEnabled = config.readEntry("SuggestionEnabled", true);
410
411 m_searchCombo->setSuggestionEnabled(m_suggestionEnabled);
412 m_openSearchManager->setSearchProvider(m_currentEngine);
413
414 m_reloadConfiguration = false;
415 setIcon();
416 }
417
reloadConfiguration()418 void SearchBarPlugin::reloadConfiguration()
419 {
420 // NOTE: We do not directly connect the dbus signal to the configurationChanged
421 // slot because our slot my be called before the filter plugins, in which case we
422 // simply end up retrieving the same configuration information from the plugin.
423 m_reloadConfiguration = true;
424 }
425
updateComboVisibility()426 void SearchBarPlugin::updateComboVisibility()
427 {
428 if (m_part.isNull()) {
429 return;
430 }
431 // NOTE: We hide the search combobox if the embedded kpart is ReadWrite
432 // because web browsers by their very nature are ReadOnly kparts...
433 m_searchComboAction->setVisible(!m_part->inherits("ReadWritePart") &&
434 !m_searchComboAction->associatedWidgets().isEmpty());
435 m_openSearchDescs.clear();
436 }
437
focusSearchbar()438 void SearchBarPlugin::focusSearchbar()
439 {
440 m_searchCombo->setFocus(Qt::ShortcutFocusReason);
441 }
442
searchTextChanged(const QString & text)443 void SearchBarPlugin::searchTextChanged(const QString &text)
444 {
445 // Don't do anything if the user just activated the search for this text
446 // Popping up suggestions again would just lead to an annoying popup (#231213)
447 if (m_lastSearch == text) {
448 return;
449 }
450
451 // Don't do anything if the user is still pressing on the mouse button
452 if (qApp->mouseButtons()) {
453 return;
454 }
455
456 // 400 ms delay before requesting for suggestions, so we don't flood the provider with suggestion request
457 m_timer->start(400);
458 }
459
requestSuggestion()460 void SearchBarPlugin::requestSuggestion()
461 {
462 m_searchCombo->clearSuggestions();
463
464 if (m_suggestionEnabled && m_searchMode != FindInThisPage &&
465 m_openSearchManager->isSuggestionAvailable() &&
466 !m_searchCombo->lineEdit()->text().isEmpty()) {
467 m_openSearchManager->requestSuggestion(m_searchCombo->lineEdit()->text());
468 }
469 }
470
enableSuggestion(bool enable)471 void SearchBarPlugin::enableSuggestion(bool enable)
472 {
473 m_suggestionEnabled = enable;
474 }
475
HTMLDocLoaded()476 void SearchBarPlugin::HTMLDocLoaded()
477 {
478 if (m_part.isNull() || m_part->url().host().isEmpty()) {
479 return;
480 }
481
482 // Testcase for this code: http://search.iwsearch.net
483 KParts::HtmlExtension *ext = KParts::HtmlExtension::childObject(m_part);
484 KParts::SelectorInterface *selectorInterface = qobject_cast<KParts::SelectorInterface *>(ext);
485
486 if (selectorInterface) {
487 //if (headElelement.getAttribute("profile") != "http://a9.com/-/spec/opensearch/1.1/") {
488 // kWarning() << "Warning: there is no profile attribute or wrong profile attribute in <head>, as specified by open search specification 1.1";
489 //}
490 const QString query(QStringLiteral("head > link[rel=\"search\"][type=\"application/opensearchdescription+xml\"]"));
491 const QList<KParts::SelectorInterface::Element> linkNodes = selectorInterface->querySelectorAll(query, KParts::SelectorInterface::EntireContent);
492 //qCDebug(SEARCHBAR_LOG) << "Found" << linkNodes.length() << "links in" << m_part->url();
493 Q_FOREACH (const KParts::SelectorInterface::Element &link, linkNodes) {
494 const QString title = link.attribute(QStringLiteral("title"));
495 const QString href = link.attribute(QStringLiteral("href"));
496 //qCDebug(SEARCHBAR_LOG) << "Found opensearch" << title << href;
497 m_openSearchDescs.insert(title, href);
498 // TODO associate this with m_part; we can get descs from multiple tabs here...
499 }
500 }
501 }
502
openSearchEngineAdded(const QString & name,const QString & searchUrl,const QString & fileName)503 void SearchBarPlugin::openSearchEngineAdded(const QString &name, const QString &searchUrl, const QString &fileName)
504 {
505 //qCDebug(SEARCHBAR_LOG) << "New Open Search Engine Added: " << name << ", searchUrl " << searchUrl;
506
507 KConfig _service(m_searchProvidersDir + fileName + ".desktop", KConfig::SimpleConfig);
508 KConfigGroup service(&_service, "Desktop Entry");
509 service.writeEntry("Type", "Service");
510 service.writeEntry("ServiceTypes", "SearchProvider");
511 service.writeEntry("Name", name);
512 service.writeEntry("Query", searchUrl);
513 service.writeEntry("Keys", fileName);
514 // TODO
515 service.writeEntry("Charset", "" /* provider->charset() */);
516
517 // we might be overwriting a hidden entry
518 service.writeEntry("Hidden", false);
519
520 // Show the add web shortcut widget
521 if (!m_addWSWidget) {
522 m_addWSWidget = new WebShortcutWidget(m_searchCombo);
523 m_addWSWidget->setWindowFlags(Qt::Popup);
524
525 connect(m_addWSWidget, &WebShortcutWidget::webShortcutSet, this, &SearchBarPlugin::webShortcutSet);
526 }
527
528 QPoint pos = m_searchCombo->mapToGlobal(QPoint(m_searchCombo->width() - m_addWSWidget->width(), m_searchCombo->height() + 1));
529 m_addWSWidget->setGeometry(QRect(pos, m_addWSWidget->size()));
530 m_addWSWidget->show(name, fileName);
531 }
532
webShortcutSet(const QString & name,const QString & webShortcut,const QString & fileName)533 void SearchBarPlugin::webShortcutSet(const QString &name, const QString &webShortcut, const QString &fileName)
534 {
535 Q_UNUSED(name);
536 KConfig _service(m_searchProvidersDir + fileName + ".desktop", KConfig::SimpleConfig);
537 KConfigGroup service(&_service, "Desktop Entry");
538 service.writeEntry("Keys", webShortcut);
539 _service.sync();
540
541 // Update filters in running applications including ourselves...
542 QDBusConnection::sessionBus().send(QDBusMessage::createSignal(QStringLiteral("/"), QStringLiteral("org.kde.KUriFilterPlugin"), QStringLiteral("configure")));
543
544 // If the providers changed, tell sycoca to rebuild its database...
545 KBuildSycocaProgressDialog::rebuildKSycoca(m_searchCombo);
546 }
547
HTMLLoadingStarted()548 void SearchBarPlugin::HTMLLoadingStarted()
549 {
550 // reset the open search availability, so that if there is previously detected engine,
551 // it will not be shown
552 m_openSearchDescs.clear();
553 }
554
addSearchSuggestion(const QStringList & suggestions)555 void SearchBarPlugin::addSearchSuggestion(const QStringList &suggestions)
556 {
557 m_searchCombo->setSuggestionItems(suggestions);
558 }
559
SearchBarCombo(QWidget * parent)560 SearchBarCombo::SearchBarCombo(QWidget *parent)
561 : KHistoryComboBox(true, parent)
562 {
563 setDuplicatesEnabled(false);
564 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
565 setMaximumWidth(300);
566 connect(this, &KHistoryComboBox::cleared, this, &SearchBarCombo::historyCleared);
567
568 Q_ASSERT(useCompletion());
569
570 KConfigGroup config(KSharedConfig::openConfig(), "SearchBar");
571 setCompletionMode(static_cast<KCompletion::CompletionMode>(config.readEntry("CompletionMode", static_cast<int>(KCompletion::CompletionPopup))));
572 const QStringList list = config.readEntry("History list", QStringList());
573 setHistoryItems(list, true);
574 Q_ASSERT(currentText().isEmpty()); // KHistoryComboBox calls clearEditText
575
576 m_enableAction = new QAction(i18n("Enable Suggestion"), this);
577 m_enableAction->setCheckable(true);
578 connect(m_enableAction, &QAction::toggled, this, &SearchBarCombo::suggestionEnabled);
579
580 connect(this, &KComboBox::aboutToShowContextMenu, this, &SearchBarCombo::addEnableMenuItem);
581
582 // use our own item delegate to display our fancy stuff :D
583 KCompletionBox *box = completionBox();
584 box->setItemDelegate(new SearchBarItemDelegate(this));
585 connect(lineEdit(), &QLineEdit::textEdited, box, &KCompletionBox::setCancelledText);
586 }
587
~SearchBarCombo()588 SearchBarCombo::~SearchBarCombo()
589 {
590 KConfigGroup config(KSharedConfig::openConfig(), "SearchBar");
591 config.writeEntry("History list", historyItems());
592 const int mode = completionMode();
593 config.writeEntry("CompletionMode", mode);
594 delete m_enableAction;
595 }
596
icon() const597 const QPixmap &SearchBarCombo::icon() const
598 {
599 return m_icon;
600 }
601
setIcon(const QPixmap & icon)602 void SearchBarCombo::setIcon(const QPixmap &icon)
603 {
604 m_icon = icon;
605 const QString editText = currentText();
606 if (count() == 0) {
607 insertItem(0, m_icon, nullptr);
608 } else {
609 for (int i = 0; i < count(); i++) {
610 setItemIcon(i, m_icon);
611 }
612 }
613 setEditText(editText);
614 }
615
setSuggestionEnabled(bool enable)616 void SearchBarCombo::setSuggestionEnabled(bool enable)
617 {
618 m_enableAction->setChecked(enable);
619 }
620
findHistoryItem(const QString & searchText)621 int SearchBarCombo::findHistoryItem(const QString &searchText)
622 {
623 for (int i = 0; i < count(); i++) {
624 if (itemText(i) == searchText) {
625 return i;
626 }
627 }
628
629 return -1;
630 }
631
mousePressEvent(QMouseEvent * e)632 void SearchBarCombo::mousePressEvent(QMouseEvent *e)
633 {
634 QStyleOptionComplex opt;
635 int x0 = QStyle::visualRect(layoutDirection(), style()->subControlRect(QStyle::CC_ComboBox, &opt, QStyle::SC_ComboBoxEditField, this), rect()).x();
636
637 if (e->x() > x0 + 2 && e->x() < lineEdit()->x()) {
638 emit iconClicked();
639 e->accept();
640 } else {
641 KHistoryComboBox::mousePressEvent(e);
642 }
643 }
644
historyCleared()645 void SearchBarCombo::historyCleared()
646 {
647 setIcon(m_icon);
648 }
649
setSuggestionItems(const QStringList & suggestions)650 void SearchBarCombo::setSuggestionItems(const QStringList &suggestions)
651 {
652 if (!m_suggestions.isEmpty()) {
653 clearSuggestions();
654 }
655
656 m_suggestions = suggestions;
657 if (!suggestions.isEmpty()) {
658 const int size = completionBox()->count();
659 QListWidgetItem *item = new QListWidgetItem(suggestions.at(0));
660 item->setData(Qt::UserRole, "suggestion");
661 completionBox()->insertItem(size + 1, item);
662 const int suggestionCount = suggestions.count();
663 for (int i = 1; i < suggestionCount; i++) {
664 completionBox()->insertItem(size + 1 + i, suggestions.at(i));
665 }
666 completionBox()->popup();
667 }
668 }
669
clearSuggestions()670 void SearchBarCombo::clearSuggestions()
671 {
672 // Removing items can change the current item in completion box,
673 // which makes the lineEdit emit textEdited, and we would then
674 // re-enter this method, so block lineEdit signals.
675 const bool oldBlock = lineEdit()->blockSignals(true);
676 int size = completionBox()->count();
677 if (!m_suggestions.isEmpty() && size >= m_suggestions.count()) {
678 for (int i = size - 1; i >= size - m_suggestions.size(); i--) {
679 completionBox()->takeItem(i);
680 }
681 }
682 m_suggestions.clear();
683 lineEdit()->blockSignals(oldBlock);
684 }
685
addEnableMenuItem(QMenu * menu)686 void SearchBarCombo::addEnableMenuItem(QMenu *menu)
687 {
688 if (menu) {
689 menu->addAction(m_enableAction);
690 }
691 }
692
SearchBarItemDelegate(QObject * parent)693 SearchBarItemDelegate::SearchBarItemDelegate(QObject *parent)
694 : QItemDelegate(parent)
695 {
696 }
697
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const698 void SearchBarItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
699 {
700 QString userText = index.data(Qt::UserRole).toString();
701 QString text = index.data(Qt::DisplayRole).toString();
702
703 // Get item data
704 if (!userText.isEmpty()) {
705 // This font is for the "information" text, small size + italic + gray in color
706 QFont usrTxtFont = option.font;
707 usrTxtFont.setItalic(true);
708 usrTxtFont.setPointSize(6);
709
710 QFontMetrics usrTxtFontMetrics(usrTxtFont);
711 int width = usrTxtFontMetrics.horizontalAdvance(userText);
712 QRect rect(option.rect.x(), option.rect.y(), option.rect.width() - width, option.rect.height());
713 QFontMetrics textFontMetrics(option.font);
714 QString elidedText = textFontMetrics.elidedText(text,
715 Qt::ElideRight, option.rect.width() - width - option.decorationSize.width());
716
717 QAbstractItemModel *itemModel = const_cast<QAbstractItemModel *>(index.model());
718 itemModel->setData(index, elidedText, Qt::DisplayRole);
719 QItemDelegate::paint(painter, option, index);
720 itemModel->setData(index, text, Qt::DisplayRole);
721
722 painter->setFont(usrTxtFont);
723 painter->setPen(QPen(QColor(Qt::gray)));
724 painter->drawText(option.rect, Qt::AlignRight, userText);
725
726 // Draw a separator above this item
727 if (index.row() > 0) {
728 painter->drawLine(option.rect.x(), option.rect.y(), option.rect.x() + option.rect.width(), option.rect.y());
729 }
730 } else {
731 QItemDelegate::paint(painter, option, index);
732 }
733 }
734
enableFindInPage() const735 bool SearchBarPlugin::enableFindInPage() const
736 {
737 return true;
738 }
739
740 #include "searchbar.moc"
741