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