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