1 /* ============================================================
2 * Falkon - Qt web browser
3 * Copyright (C) 2010-2018 David Rosca <nowrep@gmail.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 * ============================================================ */
18 #include "locationcompleter.h"
19 #include "locationcompletermodel.h"
20 #include "locationcompleterview.h"
21 #include "locationcompleterrefreshjob.h"
22 #include "locationbar.h"
23 #include "mainapplication.h"
24 #include "browserwindow.h"
25 #include "tabbedwebview.h"
26 #include "tabwidget.h"
27 #include "history.h"
28 #include "bookmarks.h"
29 #include "bookmarkitem.h"
30 #include "qzsettings.h"
31 #include "opensearchengine.h"
32 #include "networkmanager.h"
33 #include "searchenginesdialog.h"
34 
35 #include <QWindow>
36 
37 LocationCompleterView* LocationCompleter::s_view = 0;
38 LocationCompleterModel* LocationCompleter::s_model = 0;
39 
LocationCompleter(QObject * parent)40 LocationCompleter::LocationCompleter(QObject* parent)
41     : QObject(parent)
42     , m_window(0)
43     , m_locationBar(0)
44     , m_lastRefreshTimestamp(0)
45     , m_popupClosed(false)
46 {
47     if (!s_view) {
48         s_model = new LocationCompleterModel;
49         s_view = new LocationCompleterView;
50         s_view->setModel(s_model);
51     }
52 }
53 
setMainWindow(BrowserWindow * window)54 void LocationCompleter::setMainWindow(BrowserWindow* window)
55 {
56     m_window = window;
57 }
58 
setLocationBar(LocationBar * locationBar)59 void LocationCompleter::setLocationBar(LocationBar* locationBar)
60 {
61     m_locationBar = locationBar;
62 }
63 
isVisible() const64 bool LocationCompleter::isVisible() const
65 {
66     return s_view->isVisible();
67 }
68 
closePopup()69 void LocationCompleter::closePopup()
70 {
71     s_view->close();
72 }
73 
complete(const QString & string)74 void LocationCompleter::complete(const QString &string)
75 {
76     QString trimmedStr = string.trimmed();
77 
78     // Indicates that new completion was requested by user
79     // Eg. popup was not closed yet this completion session
80     m_popupClosed = false;
81 
82     emit cancelRefreshJob();
83 
84     LocationCompleterRefreshJob* job = new LocationCompleterRefreshJob(trimmedStr);
85     connect(job, &LocationCompleterRefreshJob::finished, this, &LocationCompleter::refreshJobFinished);
86     connect(this, SIGNAL(cancelRefreshJob()), job, SLOT(jobCancelled()));
87 
88     if (qzSettings->searchFromAddressBar && qzSettings->showABSearchSuggestions && trimmedStr.length() >= 2) {
89         if (!m_openSearchEngine) {
90             m_openSearchEngine = new OpenSearchEngine(this);
91             m_openSearchEngine->setNetworkAccessManager(mApp->networkManager());
92             connect(m_openSearchEngine, &OpenSearchEngine::suggestions, this, &LocationCompleter::addSuggestions);
93         }
94         m_openSearchEngine->setSuggestionsUrl(LocationBar::searchEngine().suggestionsUrl);
95         m_openSearchEngine->setSuggestionsParameters(LocationBar::searchEngine().suggestionsParameters);
96         m_suggestionsTerm = trimmedStr;
97         m_openSearchEngine->requestSuggestions(m_suggestionsTerm);
98     } else {
99         m_oldSuggestions.clear();
100     }
101 
102     // Add/update search/visit item
103     QTimer::singleShot(0, this, [=]() {
104         const QModelIndex index = s_model->index(0, 0);
105         if (index.data(LocationCompleterModel::VisitSearchItemRole).toBool()) {
106             s_model->setData(index, trimmedStr, Qt::DisplayRole);
107             s_model->setData(index, trimmedStr, LocationCompleterModel::UrlRole);
108             s_model->setData(index, m_locationBar->text(), LocationCompleterModel::SearchStringRole);
109         } else {
110             QStandardItem *item = new QStandardItem();
111             item->setText(trimmedStr);
112             item->setData(trimmedStr, LocationCompleterModel::UrlRole);
113             item->setData(m_locationBar->text(), LocationCompleterModel::SearchStringRole);
114             item->setData(true, LocationCompleterModel::VisitSearchItemRole);
115             s_model->setCompletions({item});
116             addSuggestions(m_oldSuggestions);
117         }
118         showPopup();
119         if (!s_view->currentIndex().isValid()) {
120             m_ignoreCurrentChanged = true;
121             s_view->setCurrentIndex(s_model->index(0, 0));
122             m_ignoreCurrentChanged = false;
123         }
124     });
125 }
126 
showMostVisited()127 void LocationCompleter::showMostVisited()
128 {
129     m_locationBar->setFocus();
130     complete(QString());
131 }
132 
refreshJobFinished()133 void LocationCompleter::refreshJobFinished()
134 {
135     LocationCompleterRefreshJob* job = qobject_cast<LocationCompleterRefreshJob*>(sender());
136     Q_ASSERT(job);
137 
138     // Don't show results of older jobs
139     // Also don't open the popup again when it was already closed
140     if (!job->isCanceled() && job->timestamp() > m_lastRefreshTimestamp && !m_popupClosed) {
141         s_model->setCompletions(job->completions());
142         addSuggestions(m_oldSuggestions);
143         showPopup();
144 
145         m_lastRefreshTimestamp = job->timestamp();
146 
147         if (!s_view->currentIndex().isValid() && s_model->index(0, 0).data(LocationCompleterModel::VisitSearchItemRole).toBool()) {
148             m_ignoreCurrentChanged = true;
149             s_view->setCurrentIndex(s_model->index(0, 0));
150             m_ignoreCurrentChanged = false;
151         }
152 
153         if (qzSettings->useInlineCompletion) {
154             emit showDomainCompletion(job->domainCompletion());
155         }
156 
157         s_model->setData(s_model->index(0, 0), m_locationBar->text(), LocationCompleterModel::SearchStringRole);
158     }
159 
160     job->deleteLater();
161 }
162 
slotPopupClosed()163 void LocationCompleter::slotPopupClosed()
164 {
165     m_popupClosed = true;
166     m_oldSuggestions.clear();
167 
168     disconnect(s_view, &LocationCompleterView::closed, this, &LocationCompleter::slotPopupClosed);
169     disconnect(s_view, &LocationCompleterView::indexActivated, this, &LocationCompleter::indexActivated);
170     disconnect(s_view, &LocationCompleterView::indexCtrlActivated, this, &LocationCompleter::indexCtrlActivated);
171     disconnect(s_view, &LocationCompleterView::indexShiftActivated, this, &LocationCompleter::indexShiftActivated);
172     disconnect(s_view, &LocationCompleterView::indexDeleteRequested, this, &LocationCompleter::indexDeleteRequested);
173     disconnect(s_view, &LocationCompleterView::loadRequested, this, &LocationCompleter::loadRequested);
174     disconnect(s_view, &LocationCompleterView::searchEnginesDialogRequested, this, &LocationCompleter::openSearchEnginesDialog);
175     disconnect(s_view->selectionModel(), &QItemSelectionModel::currentChanged, this, &LocationCompleter::currentChanged);
176 
177     emit popupClosed();
178 }
179 
addSuggestions(const QStringList & suggestions)180 void LocationCompleter::addSuggestions(const QStringList &suggestions)
181 {
182     const auto suggestionItems = s_model->suggestionItems();
183 
184     // Delete existing suggestions
185     for (QStandardItem *item : suggestionItems) {
186         s_model->takeRow(item->row());
187         delete item;
188     }
189 
190     // Add new suggestions
191     QList<QStandardItem*> items;
192     for (const QString &suggestion : suggestions) {
193         QStandardItem* item = new QStandardItem();
194         item->setText(suggestion);
195         item->setData(suggestion, LocationCompleterModel::TitleRole);
196         item->setData(suggestion, LocationCompleterModel::UrlRole);
197         item->setData(m_suggestionsTerm, LocationCompleterModel::SearchStringRole);
198         item->setData(true, LocationCompleterModel::SearchSuggestionRole);
199         items.append(item);
200     }
201 
202     s_model->addCompletions(items);
203     m_oldSuggestions = suggestions;
204 
205     if (!m_popupClosed) {
206         showPopup();
207     }
208 }
209 
currentChanged(const QModelIndex & index)210 void LocationCompleter::currentChanged(const QModelIndex &index)
211 {
212     if (m_ignoreCurrentChanged) {
213         return;
214     }
215 
216     QString completion = index.data().toString();
217 
218     bool completeDomain = index.data(LocationCompleterModel::VisitSearchItemRole).toBool();
219 
220     const QString originalText = s_model->index(0, 0).data(LocationCompleterModel::SearchStringRole).toString();
221 
222     // Domain completion was dismissed
223     if (completeDomain && completion == originalText) {
224         completeDomain = false;
225     }
226 
227     if (completion.isEmpty()) {
228         completeDomain = true;
229         completion = originalText;
230     }
231 
232     emit showCompletion(completion, completeDomain);
233 }
234 
indexActivated(const QModelIndex & index)235 void LocationCompleter::indexActivated(const QModelIndex &index)
236 {
237     Q_ASSERT(index.isValid());
238 
239     closePopup();
240 
241     // Clear locationbar
242     emit clearCompletion();
243 
244     bool ok;
245     const int tabPos = index.data(LocationCompleterModel::TabPositionTabRole).toInt(&ok);
246 
247     // Switch to tab with simple index activation
248     if (ok && tabPos > -1) {
249         BrowserWindow* window = static_cast<BrowserWindow*>(index.data(LocationCompleterModel::TabPositionWindowRole).value<void*>());
250         Q_ASSERT(window);
251         switchToTab(window, tabPos);
252         return;
253     }
254 
255     loadRequest(createLoadRequest(index));
256 }
257 
indexCtrlActivated(const QModelIndex & index)258 void LocationCompleter::indexCtrlActivated(const QModelIndex &index)
259 {
260     Q_ASSERT(index.isValid());
261     Q_ASSERT(m_window);
262 
263     closePopup();
264 
265     // Clear locationbar
266     emit clearCompletion();
267 
268     // Load request in new tab
269     m_window->tabWidget()->addView(createLoadRequest(index), Qz::NT_CleanSelectedTab);
270 }
271 
indexShiftActivated(const QModelIndex & index)272 void LocationCompleter::indexShiftActivated(const QModelIndex &index)
273 {
274     Q_ASSERT(index.isValid());
275 
276     closePopup();
277 
278     // Clear locationbar
279     emit clearCompletion();
280 
281     // Load request
282     if (index.data(LocationCompleterModel::VisitSearchItemRole).toBool()) {
283         loadRequest(LoadRequest(index.data(LocationCompleterModel::SearchStringRole).toUrl()));
284     } else {
285         loadRequest(createLoadRequest(index));
286     }
287 }
288 
indexDeleteRequested(const QModelIndex & index)289 void LocationCompleter::indexDeleteRequested(const QModelIndex &index)
290 {
291     if (!index.isValid()) {
292         return;
293     }
294 
295     if (index.data(LocationCompleterModel::BookmarkRole).toBool()) {
296         BookmarkItem* bookmark = static_cast<BookmarkItem*>(index.data(LocationCompleterModel::BookmarkItemRole).value<void*>());
297         mApp->bookmarks()->removeBookmark(bookmark);
298     } else if (index.data(LocationCompleterModel::HistoryRole).toBool()) {
299         int id = index.data(LocationCompleterModel::IdRole).toInt();
300         mApp->history()->deleteHistoryEntry(id);
301     } else {
302         return;
303     }
304 
305     s_view->setUpdatesEnabled(false);
306     s_model->removeRow(index.row(), index.parent());
307     s_view->setUpdatesEnabled(true);
308 
309     showPopup();
310 }
311 
createLoadRequest(const QModelIndex & index)312 LoadRequest LocationCompleter::createLoadRequest(const QModelIndex &index)
313 {
314     LoadRequest request;
315     BookmarkItem *bookmark = nullptr;
316 
317     if (index.data(LocationCompleterModel::HistoryRole).toBool()) {
318         request = index.data(LocationCompleterModel::UrlRole).toUrl();
319     } else if (index.data(LocationCompleterModel::BookmarkRole).toBool()) {
320         bookmark = static_cast<BookmarkItem*>(index.data(LocationCompleterModel::BookmarkItemRole).value<void*>());
321     } else if (index.data(LocationCompleterModel::SearchSuggestionRole).toBool()) {
322         const QString text = index.data(LocationCompleterModel::TitleRole).toString();
323         request = mApp->searchEnginesManager()->searchResult(LocationBar::searchEngine(), text);
324     } else if (index.data(LocationCompleterModel::VisitSearchItemRole).toBool()) {
325         const auto action = LocationBar::loadAction(index.data(LocationCompleterModel::SearchStringRole).toString());
326         switch (action.type) {
327         case LocationBar::LoadAction::Url:
328         case LocationBar::LoadAction::Search:
329             request = action.loadRequest;
330             break;
331         case LocationBar::LoadAction::Bookmark:
332             bookmark = action.bookmark;
333             break;
334         default:
335             break;
336         }
337     }
338 
339     if (bookmark) {
340         bookmark->updateVisitCount();
341         request = bookmark->url();
342     }
343 
344     return request;
345 }
346 
switchToTab(BrowserWindow * window,int tab)347 void LocationCompleter::switchToTab(BrowserWindow* window, int tab)
348 {
349     Q_ASSERT(window);
350     Q_ASSERT(tab >= 0);
351 
352     TabWidget* tabWidget = window->tabWidget();
353 
354     if (window->isActiveWindow() || tabWidget->currentIndex() != tab) {
355         tabWidget->setCurrentIndex(tab);
356         window->show();
357         window->activateWindow();
358         window->raise();
359     }
360     else {
361         tabWidget->webTab()->setFocus();
362     }
363 }
364 
loadRequest(const LoadRequest & request)365 void LocationCompleter::loadRequest(const LoadRequest &request)
366 {
367     closePopup();
368 
369     // Show url in locationbar
370     emit showCompletion(request.url().toString(), false);
371 
372     // Load request
373     emit loadRequested(request);
374 }
375 
openSearchEnginesDialog()376 void LocationCompleter::openSearchEnginesDialog()
377 {
378     // Clear locationbar
379     emit clearCompletion();
380 
381     SearchEnginesDialog *dialog = new SearchEnginesDialog(m_window);
382     dialog->open();
383 }
384 
showPopup()385 void LocationCompleter::showPopup()
386 {
387     Q_ASSERT(m_window);
388     Q_ASSERT(m_locationBar);
389 
390     if (!m_locationBar->hasFocus() || s_model->rowCount() == 0) {
391         s_view->close();
392         return;
393     }
394 
395     if (s_view->isVisible()) {
396         adjustPopupSize();
397         return;
398     }
399 
400     QRect popupRect(m_locationBar->mapToGlobal(m_locationBar->pos()), m_locationBar->size());
401     popupRect.setY(popupRect.bottom());
402 
403     s_view->setGeometry(popupRect);
404     s_view->setFocusProxy(m_locationBar);
405     s_view->setCurrentIndex(QModelIndex());
406 
407     connect(s_view, &LocationCompleterView::closed, this, &LocationCompleter::slotPopupClosed);
408     connect(s_view, &LocationCompleterView::indexActivated, this, &LocationCompleter::indexActivated);
409     connect(s_view, &LocationCompleterView::indexCtrlActivated, this, &LocationCompleter::indexCtrlActivated);
410     connect(s_view, &LocationCompleterView::indexShiftActivated, this, &LocationCompleter::indexShiftActivated);
411     connect(s_view, &LocationCompleterView::indexDeleteRequested, this, &LocationCompleter::indexDeleteRequested);
412     connect(s_view, &LocationCompleterView::loadRequested, this, &LocationCompleter::loadRequested);
413     connect(s_view, &LocationCompleterView::searchEnginesDialogRequested, this, &LocationCompleter::openSearchEnginesDialog);
414     connect(s_view->selectionModel(), &QItemSelectionModel::currentChanged, this, &LocationCompleter::currentChanged);
415 
416     s_view->createWinId();
417     s_view->windowHandle()->setTransientParent(m_window->windowHandle());
418 
419     adjustPopupSize();
420 }
421 
adjustPopupSize()422 void LocationCompleter::adjustPopupSize()
423 {
424     s_view->adjustSize();
425     s_view->show();
426 }
427