]> gitweb.stoutner.com Git - PrivacyBrowserPC.git/blob - src/widgets/TabWidget.cpp
Add a man file.
[PrivacyBrowserPC.git] / src / widgets / TabWidget.cpp
1 /*
2  * Copyright 2022-2023 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser PC <https://www.stoutner.com/privacy-browser-pc>.
5  *
6  * Privacy Browser PC is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser PC is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser PC.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 // Application headers.
21 #include "TabWidget.h"
22 #include "Settings.h"
23 #include "ui_AddTabWidget.h"
24 #include "ui_TabWidget.h"
25 #include "databases/CookiesDatabase.h"
26 #include "databases/DomainsDatabase.h"
27 #include "dialogs/SaveDialog.h"
28 #include "filters/MouseEventFilter.h"
29 #include "helpers/SearchEngineHelper.h"
30 #include "interceptors/UrlRequestInterceptor.h"
31 #include "windows/BrowserWindow.h"
32
33 // KDE Framework headers.
34 #include <KIO/FileCopyJob>
35 #include <KIO/JobUiDelegate>
36 #include <KNotification>
37
38 // Qt toolkit headers.
39 #include <QAction>
40 #include <QFileDialog>
41 #include <QGraphicsScene>
42 #include <QGraphicsView>
43 #include <QPrintDialog>
44 #include <QPrintPreviewDialog>
45 #include <QPrinter>
46
47 // Initialize the public static variables.
48 QString TabWidget::webEngineDefaultUserAgent = QLatin1String("");
49
50 // Construct the class.
51 TabWidget::TabWidget(QWidget *parent) : QWidget(parent)
52 {
53     // Instantiate the user agent helper.
54     userAgentHelperPointer = new UserAgentHelper();
55
56     // Instantiate the UIs.
57     Ui::TabWidget tabWidgetUi;
58     Ui::AddTabWidget addTabWidgetUi;
59
60     // Setup the main UI.
61     tabWidgetUi.setupUi(this);
62
63     // Get a handle for the tab widget.
64     tabWidgetPointer = tabWidgetUi.tabWidget;
65
66     // Setup the add tab UI.
67     addTabWidgetUi.setupUi(tabWidgetPointer);
68
69     // Get handles for the add tab widgets.
70     QWidget *addTabWidgetPointer = addTabWidgetUi.addTabQWidget;
71     QPushButton *addTabButtonPointer = addTabWidgetUi.addTabButton;
72
73     // Display the add tab widget.
74     tabWidgetPointer->setCornerWidget(addTabWidgetPointer);
75
76     // Add the first tab.
77     addFirstTab();
78
79     // Process tab events.
80     connect(tabWidgetPointer, SIGNAL(currentChanged(int)), this, SLOT(updateUiWithTabSettings()));
81     connect(addTabButtonPointer, SIGNAL(clicked()), this, SLOT(addTab()));
82     connect(tabWidgetPointer, SIGNAL(tabCloseRequested(int)), this, SLOT(deleteTab(int)));
83
84     // Store a copy of the WebEngine default user agent.
85     webEngineDefaultUserAgent = currentWebEngineProfilePointer->httpUserAgent();
86
87     // Instantiate the mouse event filter pointer.
88     MouseEventFilter *mouseEventFilterPointer = new MouseEventFilter();
89
90     // Install the mouse event filter.
91     qApp->installEventFilter(mouseEventFilterPointer);
92
93     // Process mouse forward and back commands.
94     connect(mouseEventFilterPointer, SIGNAL(mouseBack()), this, SLOT(mouseBack()));
95     connect(mouseEventFilterPointer, SIGNAL(mouseForward()), this, SLOT(mouseForward()));
96 }
97
98 TabWidget::~TabWidget()
99 {
100     // Manually delete each WebEngine page.
101     for (int i = 0; i < tabWidgetPointer->count(); ++i)
102     {
103         // Get the privacy WebEngine view.
104         PrivacyWebEngineView *privacyWebEngineViewPointer = qobject_cast<PrivacyWebEngineView *>(tabWidgetPointer->widget(i));
105
106         // Deletion the WebEngine page to prevent the following error:  `Release of profile requested but WebEnginePage still not deleted. Expect troubles !`
107         delete privacyWebEngineViewPointer->page();
108     }
109 }
110
111 // The cookie is copied instead of referenced so that changes made to the cookie do not create a race condition with the display of the cookie in the dialog.
112 void TabWidget::addCookieToStore(QNetworkCookie cookie, QWebEngineCookieStore *webEngineCookieStorePointer) const
113 {
114     // Create a url.
115     QUrl url;
116
117     // Check to see if the domain does not start with a `.` because Qt makes this harder than it should be.  <https://doc.qt.io/qt-5/qwebenginecookiestore.html#setCookie>
118     if (!cookie.domain().startsWith(QLatin1String(".")))
119     {
120         // Populate the URL.
121         url.setHost(cookie.domain());
122         url.setScheme(QLatin1String("https"));
123
124         // Clear the domain from the cookie.
125         cookie.setDomain(QLatin1String(""));
126     }
127
128     // Add the cookie to the store.
129     if (webEngineCookieStorePointer == nullptr)
130         currentWebEngineCookieStorePointer->setCookie(cookie, url);
131     else
132         webEngineCookieStorePointer->setCookie(cookie, url);
133 }
134
135 void TabWidget::addFirstTab()
136 {
137     // Create the first tab.
138     addTab();
139
140     // Update the UI with the tab settings.
141     updateUiWithTabSettings();
142
143     // Set the focus on the current tab widget.  This prevents the tab bar from showing a blue bar under the label of the first tab.
144     tabWidgetPointer->currentWidget()->setFocus();
145 }
146
147 PrivacyWebEngineView* TabWidget::addTab(const bool removeUrlLineEditFocus, const bool backgroundTab)
148 {
149     // Create a privacy WebEngine view.
150     PrivacyWebEngineView *privacyWebEngineViewPointer = new PrivacyWebEngineView();
151
152     // Add a new tab.
153     int newTabIndex = tabWidgetPointer->addTab(privacyWebEngineViewPointer, i18nc("New tab label.", "New Tab"));
154
155     // Set the default tab icon.
156     tabWidgetPointer->setTabIcon(newTabIndex, defaultTabIcon);
157
158     // Create an off-the-record profile (the default when no profile name is specified).
159     QWebEngineProfile *webEngineProfilePointer = new QWebEngineProfile(QLatin1String(""));
160
161     // Create a WebEngine page.
162     QWebEnginePage *webEnginePagePointer = new QWebEnginePage(webEngineProfilePointer);
163
164     // Set the WebEngine page.
165     privacyWebEngineViewPointer->setPage(webEnginePagePointer);
166
167     // Get handles for the web engine elements.
168     QWebEngineCookieStore *webEngineCookieStorePointer = webEngineProfilePointer->cookieStore();
169     QWebEngineSettings *webEngineSettingsPointer = webEnginePagePointer->settings();
170
171     // Update the URL line edit when the URL changes.
172     connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::urlChanged, [privacyWebEngineViewPointer, this] (const QUrl &newUrl)
173     {
174         // Only update the UI if this is the current tab.
175         if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
176         {
177             // Update the URL line edit.
178             emit updateUrlLineEdit(newUrl);
179
180             // Update the status of the forward and back buttons.
181             emit updateBackAction(currentWebEngineHistoryPointer->canGoBack());
182             emit updateForwardAction(currentWebEngineHistoryPointer->canGoForward());
183         }
184
185         // Reapply the zoom factor.  This is a bug in QWebEngineView that resets the zoom with every load.  It can be removed once <https://redmine.stoutner.com/issues/799> is fixed.
186         privacyWebEngineViewPointer->setZoomFactor(currentZoomFactor);
187     });
188
189     // Update the progress bar when a load is started.
190     connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::loadStarted, [privacyWebEngineViewPointer, this] ()
191     {
192         // Store the load progress.
193         privacyWebEngineViewPointer->loadProgressInt = 0;
194
195         // Show the progress bar if this is the current tab.
196         if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
197             emit showProgressBar(0);
198     });
199
200     // Update the progress bar when a load progresses.
201     connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::loadProgress, [privacyWebEngineViewPointer, this] (const int progress)
202     {
203         // Store the load progress.
204         privacyWebEngineViewPointer->loadProgressInt = progress;
205
206         // Update the progress bar if this is the current tab.
207         if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
208             emit showProgressBar(progress);
209     });
210
211     // Update the progress bar when a load finishes.
212     connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::loadFinished, [privacyWebEngineViewPointer, this] ()
213     {
214         // Store the load progress.
215         privacyWebEngineViewPointer->loadProgressInt = -1;
216
217         // Hide the progress bar if this is the current tab.
218         if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
219             emit hideProgressBar();
220     });
221
222     // Update the zoom factor when changed by CTRL-Scrolling.  This can be modified when <https://redmine.stoutner.com/issues/845> is fixed.
223     connect(webEnginePagePointer, &QWebEnginePage::contentsSizeChanged, [webEnginePagePointer, this] ()
224     {
225         // Only update the zoom factor action text if this is the current tab.
226         if (webEnginePagePointer == currentWebEnginePagePointer)
227             emit updateZoomFactorAction(webEnginePagePointer->zoomFactor());
228     });
229
230     // Display find text results.
231     connect(webEnginePagePointer, SIGNAL(findTextFinished(const QWebEngineFindTextResult &)), this, SLOT(findTextFinished(const QWebEngineFindTextResult &)));
232
233     // Handle full screen requests.
234     connect(webEnginePagePointer, SIGNAL(fullScreenRequested(QWebEngineFullScreenRequest)), this, SLOT(fullScreenRequested(QWebEngineFullScreenRequest)));
235
236     // Listen for hovered link URLs.
237     connect(webEnginePagePointer, SIGNAL(linkHovered(const QString)), this, SLOT(pageLinkHovered(const QString)));
238
239     // Handle file downloads.
240     connect(webEngineProfilePointer, SIGNAL(downloadRequested(QWebEngineDownloadItem *)), this, SLOT(showSaveDialog(QWebEngineDownloadItem *)));
241
242     // Instantiate the URL request interceptor.
243     UrlRequestInterceptor *urlRequestInterceptorPointer = new UrlRequestInterceptor();
244
245     // Set the URL request interceptor.
246     webEngineProfilePointer->setUrlRequestInterceptor(urlRequestInterceptorPointer);
247
248     // Reapply the domain settings when the host changes.
249     connect(urlRequestInterceptorPointer, SIGNAL(applyDomainSettings(QString)), this, SLOT(applyDomainSettingsWithoutReloading(QString)));
250
251     // Set the local storage filter.
252     webEngineCookieStorePointer->setCookieFilter([privacyWebEngineViewPointer](const QWebEngineCookieStore::FilterRequest &filterRequest)
253     {
254         // Block all third party local storage requests, including the sneaky ones that don't register a first party URL.
255         if (filterRequest.thirdParty || (filterRequest.firstPartyUrl == QStringLiteral("")))
256         {
257             //qDebug().noquote().nospace() << "Third-party request blocked:  " << filterRequest.origin;
258
259             // Return false.
260             return false;
261         }
262
263         // Allow the request if local storage is enabled.
264         if (privacyWebEngineViewPointer->localStorageEnabled)
265         {
266             //qDebug().noquote().nospace() << "Request allowed by local storage:  " << filterRequest.origin;
267
268             // Return true.
269             return true;
270         }
271
272         //qDebug().noquote().nospace() << "Request blocked by default:  " << filterRequest.origin;
273
274         // Block any remaining local storage requests.
275         return false;
276     });
277
278     // Disable JavaScript by default (this prevents JavaScript from being enabled on a new tab before domain settings are loaded).
279     webEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, false);
280
281     // Don't allow JavaScript to open windows.
282     webEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false);
283
284     // Allow keyboard navigation.
285     webEngineSettingsPointer->setAttribute(QWebEngineSettings::SpatialNavigationEnabled, true);
286
287     // Enable full screen support.
288     webEngineSettingsPointer->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, true);
289
290     // Require user interaction to play media.
291     webEngineSettingsPointer->setAttribute(QWebEngineSettings::PlaybackRequiresUserGesture, true);
292
293     // Limit WebRTC to public IP addresses.
294     webEngineSettingsPointer->setAttribute(QWebEngineSettings::WebRTCPublicInterfacesOnly, true);
295
296     // Enable the PDF viewer (it should be enabled by default, but it is nice to be explicit in case the defaults change).
297     webEngineSettingsPointer->setAttribute(QWebEngineSettings::PdfViewerEnabled, true);
298
299     // Plugins must be enabled for the PDF viewer to work.  <https://doc.qt.io/qt-5/qtwebengine-features.html#pdf-file-viewing>
300     webEngineSettingsPointer->setAttribute(QWebEngineSettings::PluginsEnabled, true);
301
302     // Update the cookies action.
303     connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::updateCookiesAction, [privacyWebEngineViewPointer, this] (const int numberOfCookies)
304     {
305         // Update the cookie action if the specified privacy WebEngine view is the current privacy WebEngine view.
306         if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
307             emit updateCookiesAction(numberOfCookies);
308     });
309
310     // Process cookie changes.
311     connect(webEngineCookieStorePointer, SIGNAL(cookieAdded(QNetworkCookie)), privacyWebEngineViewPointer, SLOT(addCookieToList(QNetworkCookie)));
312     connect(webEngineCookieStorePointer, SIGNAL(cookieRemoved(QNetworkCookie)), privacyWebEngineViewPointer, SLOT(removeCookieFromList(QNetworkCookie)));
313
314     // Get a list of durable cookies.
315     QList<QNetworkCookie*> *durableCookiesListPointer = CookiesDatabase::getCookies();
316
317     // Add the durable cookies to the store.
318     for (QNetworkCookie *cookiePointer : *durableCookiesListPointer)
319         addCookieToStore(*cookiePointer, webEngineCookieStorePointer);
320
321     // Update the title when it changes.
322     connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::titleChanged, [this, privacyWebEngineViewPointer] (const QString &title)
323     {
324         // Get the index for this tab.
325         int tabIndex = tabWidgetPointer->indexOf(privacyWebEngineViewPointer);
326
327         // Update the title for this tab.
328         tabWidgetPointer->setTabText(tabIndex, title);
329
330         // Update the window title if this is the current tab.
331         if (tabIndex == tabWidgetPointer->currentIndex())
332             emit updateWindowTitle(title);
333     });
334
335     // Update the icon when it changes.
336     connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::iconChanged, [privacyWebEngineViewPointer, this] (const QIcon &icon)
337     {
338         // Get the index for this tab.
339         int tabIndex = tabWidgetPointer->indexOf(privacyWebEngineViewPointer);
340
341         // Update the icon for this tab.
342         if (icon.isNull())
343             tabWidgetPointer->setTabIcon(tabIndex, defaultTabIcon);
344         else
345             tabWidgetPointer->setTabIcon(tabIndex, icon);
346     });
347
348     // Enable spell checking.
349     webEngineProfilePointer->setSpellCheckEnabled(true);
350
351     // Set the spell check language.
352     webEngineProfilePointer->setSpellCheckLanguages(Settings::spellCheckLanguages());
353
354     // Populate the zoom factor.  This is necessary if a URL is being loaded, like a local URL, that does not trigger `applyDomainSettings()`.
355     privacyWebEngineViewPointer->setZoomFactor(Settings::zoomFactor());
356
357     // Move to the new tab if it is not a background tab.
358     if (!backgroundTab)
359         tabWidgetPointer->setCurrentIndex(newTabIndex);
360
361     // Clear the URL line edit focus so that it populates correctly when opening a new tab from the context menu.
362     if (removeUrlLineEditFocus)
363         emit clearUrlLineEditFocus();
364
365     // Return the privacy WebEngine view pointer.
366     return privacyWebEngineViewPointer;
367 }
368
369 void TabWidget::applyApplicationSettings()
370 {
371     // Set the tab position.
372     if (Settings::tabsOnTop())
373         tabWidgetPointer->setTabPosition(QTabWidget::North);
374     else
375         tabWidgetPointer->setTabPosition(QTabWidget::South);
376
377     // Set the search engine URL.
378     searchEngineUrl = SearchEngineHelper::getSearchUrl(Settings::searchEngine());
379
380     // Emit the update search engine actions signal.
381     emit updateSearchEngineActions(Settings::searchEngine(), true);
382 }
383
384 // This exists as a separate function from `applyDomainSettings()` so it can be listed as a slot and function without the need for a boolean argument.
385 // Once <https://redmine.stoutner.com/issues/799> has been resolved this can be `const`.
386 void TabWidget::applyDomainSettingsAndReload()
387 {
388     // Apply the domain settings.  `true` reloads the website.
389     applyDomainSettings(currentPrivacyWebEngineViewPointer->url().host(), true);
390 }
391
392 // This exists as a separate function from `applyDomainSettings()` so it can be listed as a slot and function without the need for a boolean argument.
393 // Once <https://redmine.stoutner.com/issues/799> has been resolved this can be `const`.
394 void TabWidget::applyDomainSettingsWithoutReloading(const QString &hostname)
395 {
396     // Apply the domain settings  `false` does not reload the website.
397     applyDomainSettings(hostname, false);
398 }
399
400 // Once <https://redmine.stoutner.com/issues/799> has been resolved this can be `const`.
401 void TabWidget::applyDomainSettings(const QString &hostname, const bool reloadWebsite)
402 {
403     // Get the record for the hostname.
404     QSqlQuery domainQuery = DomainsDatabase::getDomainQuery(hostname);
405
406     // Check if the hostname has domain settings.
407     if (domainQuery.isValid())  // The hostname has domain settings.
408     {
409         // Get the domain record.
410         QSqlRecord domainRecord = domainQuery.record();
411
412         // Store the domain settings name.
413         currentPrivacyWebEngineViewPointer->domainSettingsName = domainRecord.field(DomainsDatabase::DOMAIN_NAME).value().toString();
414
415         // Set the JavaScript status.
416         switch (domainRecord.field(DomainsDatabase::JAVASCRIPT).value().toInt())
417         {
418             // Set the default JavaScript status.
419             case (DomainsDatabase::SYSTEM_DEFAULT):
420             {
421                 currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, Settings::javaScriptEnabled());
422
423                 break;
424             }
425
426             // Disable JavaScript.
427             case (DomainsDatabase::DISABLED):
428             {
429                 currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, false);
430
431                 break;
432             }
433
434             // Enable JavaScript.
435             case (DomainsDatabase::ENABLED):
436             {
437                 currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
438
439                 break;
440             }
441         }
442
443         // Set the local storage status.
444         switch (domainRecord.field(DomainsDatabase::LOCAL_STORAGE).value().toInt())
445         {
446             // Set the default local storage status.
447             case (DomainsDatabase::SYSTEM_DEFAULT):
448             {
449                 currentPrivacyWebEngineViewPointer->localStorageEnabled = Settings::localStorageEnabled();
450
451                 break;
452             }
453
454             // Disable local storage.
455             case (DomainsDatabase::DISABLED):
456             {
457                 currentPrivacyWebEngineViewPointer->localStorageEnabled = false;
458
459                 break;
460             }
461
462             // Enable local storage.
463             case (DomainsDatabase::ENABLED):
464             {
465                 currentPrivacyWebEngineViewPointer->localStorageEnabled = true;
466
467                 break;
468             }
469         }
470
471         // Set the DOM storage status.
472         switch (domainRecord.field(DomainsDatabase::DOM_STORAGE).value().toInt())
473         {
474             // Set the default DOM storage status.  QWebEngineSettings confusingly calls this local storage.
475             case (DomainsDatabase::SYSTEM_DEFAULT):
476             {
477                 currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, Settings::domStorageEnabled());
478
479                 break;
480             }
481
482             // Disable DOM storage.  QWebEngineSettings confusingly calls this local storage.
483             case (DomainsDatabase::DISABLED):
484             {
485                 currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, false);
486
487                 break;
488             }
489
490             // Enable DOM storage.  QWebEngineSettings confusingly calls this local storage.
491             case (DomainsDatabase::ENABLED):
492             {
493                 currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, true);
494
495                 break;
496             }
497         }
498
499         // Set the user agent.
500         currentWebEngineProfilePointer->setHttpUserAgent(UserAgentHelper::getResultingDomainSettingsUserAgent(domainRecord.field(DomainsDatabase::USER_AGENT).value().toString()));
501
502         // Check if a custom zoom factor is set.
503         if (domainRecord.field(DomainsDatabase::ZOOM_FACTOR).value().toInt())
504         {
505             // Store the current zoom factor.
506             currentZoomFactor = domainRecord.field(DomainsDatabase::CUSTOM_ZOOM_FACTOR).value().toDouble();
507         }
508         else
509         {
510             // Reset the current zoom factor.
511             currentZoomFactor = Settings::zoomFactor();
512         }
513
514         // Set the zoom factor.    The use of `currentZoomFactor` can be removed once <https://redmine.stoutner.com/issues/799> has been resolved.
515         currentPrivacyWebEngineViewPointer->setZoomFactor(currentZoomFactor);
516     }
517     else  // The hostname does not have domain settings.
518     {
519         // Reset the domain settings name.
520         currentPrivacyWebEngineViewPointer->domainSettingsName = QLatin1String("");
521
522         // Set the JavaScript status.
523         currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, Settings::javaScriptEnabled());
524
525         // Set the local storage status.
526         currentPrivacyWebEngineViewPointer->localStorageEnabled = Settings::localStorageEnabled();
527
528         // Set DOM storage.  In QWebEngineSettings it is called Local Storage.
529         currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, Settings::domStorageEnabled());
530
531         // Set the user agent.
532         currentWebEngineProfilePointer->setHttpUserAgent(UserAgentHelper::getUserAgentFromDatabaseName(Settings::userAgent()));
533
534         // Store the current zoom factor.  This can be removed once <https://redmine.stoutner.com/issues/799> has been resolved.
535         currentZoomFactor = Settings::zoomFactor();
536
537         // Set the zoom factor.
538         currentPrivacyWebEngineViewPointer->setZoomFactor(Settings::zoomFactor());
539     }
540
541     // Update the UI.
542     emit updateDomainSettingsIndicator(currentPrivacyWebEngineViewPointer->domainSettingsName != QLatin1String(""));
543     emit updateJavaScriptAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::JavascriptEnabled));
544     emit updateLocalStorageAction(currentPrivacyWebEngineViewPointer->localStorageEnabled);
545     emit updateDomStorageAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::LocalStorageEnabled));
546     emit updateUserAgentActions(currentWebEngineProfilePointer->httpUserAgent(), true);
547     emit updateZoomFactorAction(currentPrivacyWebEngineViewPointer->zoomFactor());
548
549     // Reload the website if requested.
550     if (reloadWebsite)
551         currentPrivacyWebEngineViewPointer->reload();
552 }
553
554 void TabWidget::applyOnTheFlySearchEngine(QAction *searchEngineActionPointer)
555 {
556     // Store the search engine name.
557     QString searchEngineName = searchEngineActionPointer->text();
558
559     // Strip out any `&` characters.
560     searchEngineName.remove('&');
561
562     // Store the search engine string.
563     searchEngineUrl = SearchEngineHelper::getSearchUrl(searchEngineName);
564
565     // Update the search engine actions.
566     emit updateSearchEngineActions(searchEngineName, false);
567 }
568
569 void TabWidget::applyOnTheFlyUserAgent(QAction *userAgentActionPointer) const
570 {
571     // Get the user agent name.
572     QString userAgentName = userAgentActionPointer->text();
573
574     // Strip out any `&` characters.
575     userAgentName.remove('&');
576
577     // Apply the user agent.
578     currentWebEngineProfilePointer->setHttpUserAgent(userAgentHelperPointer->getUserAgentFromTranslatedName(userAgentName));
579
580     // Update the user agent actions.
581     emit updateUserAgentActions(currentWebEngineProfilePointer->httpUserAgent(), false);
582
583     // Reload the website.
584     currentPrivacyWebEngineViewPointer->reload();
585 }
586
587 // This can be const once <https://redmine.stoutner.com/issues/799> has been resolved.
588 void TabWidget::applyOnTheFlyZoomFactor(const double &zoomFactor)
589 {
590     // Update the current zoom factor.  This can be removed once <https://redmine.stoutner.com/issues/799> has been resolved.
591     currentZoomFactor = zoomFactor;
592
593     // Set the zoom factor.
594     currentPrivacyWebEngineViewPointer->setZoomFactor(zoomFactor);
595 }
596
597 void TabWidget::applySpellCheckLanguages() const
598 {
599     // Get the number of tab.
600     int numberOfTabs = tabWidgetPointer->count();
601
602     // Set the spell check languages for each tab.
603     for (int i = 0; i < numberOfTabs; ++i)
604     {
605         // Get the WebEngine view pointer.
606         PrivacyWebEngineView *webEngineViewPointer = qobject_cast<PrivacyWebEngineView *>(tabWidgetPointer->currentWidget());
607
608         // Get the WebEngine page pointer.
609         QWebEnginePage *webEnginePagePointer = webEngineViewPointer->page();
610
611         // Get the WebEngine profile pointer.
612         QWebEngineProfile *webEngineProfilePointer = webEnginePagePointer->profile();
613
614         // Set the spell check languages.
615         webEngineProfilePointer->setSpellCheckLanguages(Settings::spellCheckLanguages());
616     }
617 }
618
619 void TabWidget::back() const
620 {
621     // Go back.
622     currentPrivacyWebEngineViewPointer->back();
623 }
624
625 void TabWidget::deleteAllCookies() const
626 {
627     // Delete all the cookies.
628     currentWebEngineCookieStorePointer->deleteAllCookies();
629 }
630
631 void TabWidget::deleteCookieFromStore(const QNetworkCookie &cookie) const
632 {
633     // Delete the cookie.
634     currentWebEngineCookieStorePointer->deleteCookie(cookie);
635 }
636
637 void TabWidget::deleteTab(const int tabIndex)
638 {
639     // Get the privacy WebEngine view.
640     PrivacyWebEngineView *privacyWebEngineViewPointer = qobject_cast<PrivacyWebEngineView *>(tabWidgetPointer->widget(tabIndex));
641
642     // Proccess the tab delete according to the number of tabs.
643     if (tabWidgetPointer->count() > 1)  // There is more than one tab.
644     {
645         // Delete the tab.
646         tabWidgetPointer->removeTab(tabIndex);
647
648         // Delete the WebEngine page to prevent the following error:  `Release of profile requested but WebEnginePage still not deleted. Expect troubles !`
649         delete privacyWebEngineViewPointer->page();
650
651         // Delete the privacy WebEngine view.
652         delete privacyWebEngineViewPointer;
653     }
654     else  // There is only one tab.
655     {
656         // Close Privacy Browser.
657         window()->close();
658     }
659 }
660
661 void TabWidget::findPrevious(const QString &text) const
662 {
663     // Store the current text.
664     currentPrivacyWebEngineViewPointer->findString = text;
665
666     // Find the previous text in the current privacy WebEngine.
667     if (currentPrivacyWebEngineViewPointer->findCaseSensitive)
668         currentPrivacyWebEngineViewPointer->findText(text, QWebEnginePage::FindCaseSensitively|QWebEnginePage::FindBackward);
669     else
670         currentPrivacyWebEngineViewPointer->findText(text, QWebEnginePage::FindBackward);
671 }
672
673 void TabWidget::findText(const QString &text) const
674 {
675     // Store the current text.
676     currentPrivacyWebEngineViewPointer->findString = text;
677
678     // Find the text in the current privacy WebEngine.
679     if (currentPrivacyWebEngineViewPointer->findCaseSensitive)
680         currentPrivacyWebEngineViewPointer->findText(text, QWebEnginePage::FindCaseSensitively);
681     else
682         currentPrivacyWebEngineViewPointer->findText(text);
683
684     // Clear the currently selected text in the WebEngine page if the find text is empty.
685     if (text.isEmpty())
686         currentWebEnginePagePointer->action(QWebEnginePage::Unselect)->activate(QAction::Trigger);
687 }
688
689 void TabWidget::findTextFinished(const QWebEngineFindTextResult &findTextResult)
690 {
691     // Update the find text UI if it wasn't simply wiping the current find text selection.  Otherwise the UI temporarially flashes `0/0`.
692     if (wipingCurrentFindTextSelection)  // The current selection is being wiped.
693     {
694         // Reset the flag.
695         wipingCurrentFindTextSelection = false;
696     }
697     else  // A new search has been performed.
698     {
699         // Store the result.
700         currentPrivacyWebEngineViewPointer->findTextResult = findTextResult;
701
702         // Update the UI.
703         emit updateFindTextResults(findTextResult);
704     }
705 }
706
707 void TabWidget::forward() const
708 {
709     // Go forward.
710     currentPrivacyWebEngineViewPointer->forward();
711 }
712
713 void TabWidget::fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest) const
714 {
715     // Make it so.
716     emit fullScreenRequested(fullScreenRequest.toggleOn());
717
718     // Accept the request.
719     fullScreenRequest.accept();
720 }
721
722 std::list<QNetworkCookie>* TabWidget::getCookieList() const
723 {
724     // Return the current cookie list.
725     return currentPrivacyWebEngineViewPointer->cookieListPointer;
726 }
727
728 QString& TabWidget::getDomainSettingsName() const
729 {
730     // Return the domain settings name.
731     return currentPrivacyWebEngineViewPointer->domainSettingsName;
732 }
733
734 void TabWidget::home() const
735 {
736     // Load the homepage.
737     currentPrivacyWebEngineViewPointer->load(QUrl::fromUserInput(Settings::homepage()));
738 }
739
740 PrivacyWebEngineView* TabWidget::loadBlankInitialWebsite()
741 {
742     // Apply the application settings.
743     applyApplicationSettings();
744
745     // Return the current privacy WebEngine view pointer.
746     return currentPrivacyWebEngineViewPointer;
747 }
748
749 void TabWidget::loadInitialWebsite()
750 {
751     // Apply the application settings.
752     applyApplicationSettings();
753
754     // Get the arguments.
755     QStringList argumentsStringList = qApp->arguments();
756
757     // Check to see if the arguments lists contains a URL.
758     if (argumentsStringList.size() > 1)
759     {
760         // Load the URL from the arguments list.
761         currentPrivacyWebEngineViewPointer->load(QUrl::fromUserInput(argumentsStringList.at(1)));
762     }
763     else
764     {
765         // Load the homepage.
766         home();
767     }
768 }
769
770 void TabWidget::loadUrlFromLineEdit(QString url) const
771 {
772     // Decide if the text is more likely to be a URL or a search.
773     if (url.startsWith("file://"))  // The text is likely a file URL.
774     {
775         // Load the URL.
776         currentPrivacyWebEngineViewPointer->load(QUrl::fromUserInput(url));
777     }
778     else if (url.contains("."))  // The text is likely a URL.
779     {
780         // Check if the URL does not start with a valid protocol.
781         if (!url.startsWith("http"))
782         {
783             // Add `https://` to the beginning of the URL.
784             url = "https://" + url;
785         }
786
787         // Load the URL.
788         currentPrivacyWebEngineViewPointer->load(QUrl::fromUserInput(url));
789     }
790     else  // The text is likely a search.
791     {
792         // Load the search.
793         currentPrivacyWebEngineViewPointer->load(QUrl::fromUserInput(searchEngineUrl + url));
794     }
795 }
796
797 void TabWidget::mouseBack() const
798 {
799     // Go back if possible.
800     if (currentPrivacyWebEngineViewPointer->isActiveWindow() && currentWebEngineHistoryPointer->canGoBack())
801     {
802         // Clear the URL line edit focus.
803         emit clearUrlLineEditFocus();
804
805         // Go back.
806         currentPrivacyWebEngineViewPointer->back();
807     }
808 }
809
810 void TabWidget::mouseForward() const
811 {
812     // Go forward if possible.
813     if (currentPrivacyWebEngineViewPointer->isActiveWindow() && currentWebEngineHistoryPointer->canGoForward())
814     {
815         // Clear the URL line edit focus.
816         emit clearUrlLineEditFocus();
817
818         // Go forward.
819         currentPrivacyWebEngineViewPointer->forward();
820     }
821 }
822
823 void TabWidget::pageLinkHovered(const QString &linkUrl) const
824 {
825     // Emit a signal so that the browser window can update the status bar.
826     emit linkHovered(linkUrl);
827 }
828
829 void TabWidget::print() const
830 {
831     // Create a printer.
832     QPrinter printer;
833
834     // Set the resolution to be 300 dpi.
835     printer.setResolution(300);
836
837     // Create a printer dialog.
838     QPrintDialog printDialog(&printer, currentPrivacyWebEngineViewPointer);
839
840     // Display the dialog and print the page if instructed.
841     if (printDialog.exec() == QDialog::Accepted)
842         printWebpage(&printer);
843 }
844
845 void TabWidget::printPreview() const
846 {
847     // Create a printer.
848     QPrinter printer;
849
850     // Set the resolution to be 300 dpi.
851     printer.setResolution(300);
852
853     // Create a print preview dialog.
854     QPrintPreviewDialog printPreviewDialog(&printer, currentPrivacyWebEngineViewPointer);
855
856     // Generate the print preview.
857     connect(&printPreviewDialog, SIGNAL(paintRequested(QPrinter *)), this, SLOT(printWebpage(QPrinter *)));
858
859     // Display the dialog.
860     printPreviewDialog.exec();
861 }
862
863 void TabWidget::printWebpage(QPrinter *printerPointer) const
864 {
865     // Create an event loop.  For some reason, the print preview doesn't produce any output unless it is run inside an event loop.
866     QEventLoop eventLoop;
867
868     // Print the webpage, converting the callback above into a `QWebEngineCallback<bool>`.
869     // Printing requires that the printer be a pointer, not a reference, or it will crash with much cursing.
870     currentWebEnginePagePointer->print(printerPointer, [&eventLoop](bool printSuccess)
871     {
872         // Instruct the compiler to ignore the unused parameter.
873         (void) printSuccess;
874
875         // Quit the loop.
876         eventLoop.quit();
877     });
878
879     // Execute the loop.
880     eventLoop.exec();
881 }
882
883 void TabWidget::refresh() const
884 {
885     // Reload the website.
886     currentPrivacyWebEngineViewPointer->reload();
887 }
888
889 void TabWidget::setTabBarVisible(const bool visible) const
890 {
891     // Set the tab bar visibility.
892     tabWidgetPointer->tabBar()->setVisible(visible);
893 }
894
895 void TabWidget::showSaveDialog(QWebEngineDownloadItem *webEngineDownloadItemPointer)
896 {
897     // Get the download attributes.
898     QUrl downloadUrl = webEngineDownloadItemPointer->url();
899     QString mimeTypeString = webEngineDownloadItemPointer->mimeType();
900     QString suggestedFileName = webEngineDownloadItemPointer->suggestedFileName();
901     int totalBytes = webEngineDownloadItemPointer->totalBytes();
902
903     // Check to see if local storage (cookies) is enabled.
904     if (currentPrivacyWebEngineViewPointer->localStorageEnabled)  // Local storage (cookies) is enabled.  Use WebEngine's downloader.
905     {
906         // Instantiate the save dialog.
907         SaveDialog *saveDialogPointer = new SaveDialog(downloadUrl, mimeTypeString, totalBytes);
908
909         // Display the save dialog.
910         int saveDialogResult = saveDialogPointer->exec();
911
912         // Process the save dialog results.
913         if (saveDialogResult == QDialog::Accepted)  // Save was selected.
914         {
915             // Get the download directory.
916             QString downloadDirectory = Settings::downloadLocation();
917
918             // Resolve the system download directory if specified.
919             if (downloadDirectory == QLatin1String("System Download Directory"))
920                 downloadDirectory = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
921
922             // Display a save file dialog.
923             QString saveFilePath = QFileDialog::getSaveFileName(this, i18nc("Save file dialog caption", "Save File"), downloadDirectory + QLatin1Char('/') + suggestedFileName);
924
925             // Process the save file path.
926             if (!saveFilePath.isEmpty())  // The file save path is populated.
927             {
928                 // Create a save file path file info.
929                 QFileInfo saveFilePathFileInfo = QFileInfo(saveFilePath);
930
931                 // Get the canonical save path and file name.
932                 QString absoluteSavePath = saveFilePathFileInfo.absolutePath();
933                 QString saveFileName = saveFilePathFileInfo.fileName();
934
935                 // Set the download directory and file name.
936                 webEngineDownloadItemPointer->setDownloadDirectory(absoluteSavePath);
937                 webEngineDownloadItemPointer->setDownloadFileName(saveFileName);
938
939                 // Create a file download notification.
940                 KNotification *fileDownloadNotificationPointer = new KNotification(QLatin1String("FileDownload"));
941
942                 // Set the notification title.
943                 fileDownloadNotificationPointer->setTitle(i18nc("Download notification title", "Download"));
944
945                 // Set the notification text.
946                 fileDownloadNotificationPointer->setText(i18nc("Downloading notification text", "Downloading %1", saveFileName));
947
948                 // Set the notification icon.
949                 fileDownloadNotificationPointer->setIconName(QLatin1String("download"));
950
951                 // Set the action list cancel button.
952                 fileDownloadNotificationPointer->setActions(QStringList({i18nc("Download notification action","Cancel")}));
953
954                 // Set the notification to display indefinitely.
955                 fileDownloadNotificationPointer->setFlags(KNotification::Persistent);
956
957                 // Prevent the notification from being autodeleted if it is closed.  Otherwise, the updates to the notification below cause a crash.
958                 fileDownloadNotificationPointer->setAutoDelete(false);
959
960                 // Display the notification.
961                 fileDownloadNotificationPointer->sendEvent();
962
963                 // Handle clicks on the cancel button.
964                 connect(fileDownloadNotificationPointer, &KNotification::action1Activated, [webEngineDownloadItemPointer, saveFileName] ()
965                 {
966                     // Cancel the download.
967                     webEngineDownloadItemPointer->cancel();
968
969                     // Create a file download notification.
970                     KNotification *canceledDownloadNotificationPointer = new KNotification(QLatin1String("FileDownload"));
971
972                     // Set the notification title.
973                     canceledDownloadNotificationPointer->setTitle(i18nc("Download notification title", "Download"));
974
975                     // Set the new text.
976                     canceledDownloadNotificationPointer->setText(i18nc("Download canceled notification", "%1 download canceled", saveFileName));
977
978                     // Set the notification icon.
979                     canceledDownloadNotificationPointer->setIconName(QLatin1String("download"));
980
981                     // Display the notification.
982                     canceledDownloadNotificationPointer->sendEvent();
983                 });
984
985                 // Update the notification when the download progresses.
986                 connect(webEngineDownloadItemPointer, &QWebEngineDownloadItem::downloadProgress, [fileDownloadNotificationPointer, saveFileName] (qint64 bytesReceived, qint64 totalBytes)
987                 {
988                     // Set the new text.  Total bytes will be 0 if the download size is unknown.
989                     if (totalBytes > 0)
990                     {
991                         // Calculate the download percentage.
992                         int downloadPercentage = 100 * bytesReceived / totalBytes;
993
994                         // Set the file download notification text.
995                         fileDownloadNotificationPointer->setText(i18nc("Download progress notification text", "%1\% of %2 downloaded (%3 of %4 bytes)", downloadPercentage, saveFileName,
996                                                                     bytesReceived, totalBytes));
997                     }
998                     else
999                     {
1000                         // Set the file download notification text.
1001                         fileDownloadNotificationPointer->setText(i18nc("Download progress notification text", "%1:  %2 bytes downloaded", saveFileName, bytesReceived));
1002                     }
1003
1004                     // Display the updated notification.
1005                     fileDownloadNotificationPointer->update();
1006                 });
1007
1008                 // Update the notification when the download finishes.  The save file name must be copied into the lambda or a crash occurs.
1009                 connect(webEngineDownloadItemPointer, &QWebEngineDownloadItem::finished, [fileDownloadNotificationPointer, saveFileName, saveFilePath] ()
1010                 {
1011                     // Set the new text.
1012                     fileDownloadNotificationPointer->setText(i18nc("Download finished notification text", "%1 download finished", saveFileName));
1013
1014                     // Set the URL so the file options will be displayed.
1015                     fileDownloadNotificationPointer->setUrls(QList<QUrl> {QUrl(saveFilePath)});
1016
1017                     // Remove the actions from the notification.
1018                     fileDownloadNotificationPointer->setActions(QStringList());
1019
1020                     // Set the notification to disappear after a timeout.
1021                     fileDownloadNotificationPointer->setFlags(KNotification::CloseOnTimeout);
1022
1023                     // Display the updated notification.
1024                     fileDownloadNotificationPointer->update();
1025                 });
1026
1027                 // Start the download.
1028                 webEngineDownloadItemPointer->accept();
1029             }
1030             else  // The file save path is not populated.
1031             {
1032                 // Cancel the download.
1033                 webEngineDownloadItemPointer->cancel();
1034             }
1035         }
1036         else  // Cancel was selected.
1037         {
1038             // Cancel the download.
1039             webEngineDownloadItemPointer->cancel();
1040         }
1041     }
1042     else  // Local storage (cookies) is disabled.  Use KDE's native downloader.
1043           // This must use the show command to launch a separate dialog which cancels WebEngine's automatic background download of the file to a temporary location.
1044     {
1045         // Instantiate the save dialog.  `true` instructs it to use the native downloader
1046         SaveDialog *saveDialogPointer = new SaveDialog(downloadUrl, mimeTypeString, totalBytes, suggestedFileName, true);
1047
1048         // Connect the save button.
1049         connect(saveDialogPointer, SIGNAL(useNativeDownloader(QUrl &, QString &)), this, SLOT(useNativeDownloader(QUrl &, QString &)));
1050
1051         // Show the dialog.
1052         saveDialogPointer->show();
1053     }
1054 }
1055
1056 void TabWidget::toggleDomStorage() const
1057 {
1058     // Toggle DOM storage.
1059     currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, !currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::LocalStorageEnabled));
1060
1061     // Update the DOM storage action.
1062     emit updateDomStorageAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::LocalStorageEnabled));
1063
1064     // Reload the website.
1065     currentPrivacyWebEngineViewPointer->reload();
1066 }
1067
1068 void TabWidget::toggleFindCaseSensitive(const QString &text)
1069 {
1070     // Toggle find case sensitive.
1071     currentPrivacyWebEngineViewPointer->findCaseSensitive = !currentPrivacyWebEngineViewPointer->findCaseSensitive;
1072
1073     // Set the wiping current find text selection flag.
1074     wipingCurrentFindTextSelection = true;
1075
1076     // Wipe the previous search.  Otherwise currently highlighted words will remain highlighted.
1077     findText(QLatin1String(""));
1078
1079     // Update the find text.
1080     findText(text);
1081 }
1082
1083 void TabWidget::toggleJavaScript() const
1084 {
1085     // Toggle JavaScript.
1086     currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, !currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::JavascriptEnabled));
1087
1088     // Update the JavaScript action.
1089     emit updateJavaScriptAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::JavascriptEnabled));
1090
1091     // Reload the website.
1092     currentPrivacyWebEngineViewPointer->reload();
1093 }
1094
1095 void TabWidget::toggleLocalStorage()
1096 {
1097     // Toggle local storage.
1098     currentPrivacyWebEngineViewPointer->localStorageEnabled = !currentPrivacyWebEngineViewPointer->localStorageEnabled;
1099
1100     // Update the local storage action.
1101     emit updateLocalStorageAction(currentPrivacyWebEngineViewPointer->localStorageEnabled);
1102
1103     // Reload the website.
1104     currentPrivacyWebEngineViewPointer->reload();
1105 }
1106
1107 void TabWidget::updateUiWithTabSettings()
1108 {
1109     // Update the current WebEngine pointers.
1110     currentPrivacyWebEngineViewPointer = qobject_cast<PrivacyWebEngineView *>(tabWidgetPointer->currentWidget());
1111     currentWebEngineSettingsPointer = currentPrivacyWebEngineViewPointer->settings();
1112     currentWebEnginePagePointer = currentPrivacyWebEngineViewPointer->page();
1113     currentWebEngineProfilePointer = currentWebEnginePagePointer->profile();
1114     currentWebEngineHistoryPointer = currentWebEnginePagePointer->history();
1115     currentWebEngineCookieStorePointer = currentWebEngineProfilePointer->cookieStore();
1116
1117     // Clear the URL line edit focus.
1118     emit clearUrlLineEditFocus();
1119
1120     // Update the actions.
1121     emit updateBackAction(currentWebEngineHistoryPointer->canGoBack());
1122     emit updateCookiesAction(currentPrivacyWebEngineViewPointer->cookieListPointer->size());
1123     emit updateDomStorageAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::LocalStorageEnabled));
1124     emit updateForwardAction(currentWebEngineHistoryPointer->canGoForward());
1125     emit updateJavaScriptAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::JavascriptEnabled));
1126     emit updateLocalStorageAction(currentPrivacyWebEngineViewPointer->localStorageEnabled);
1127     emit updateUserAgentActions(currentWebEngineProfilePointer->httpUserAgent(), true);
1128     emit updateZoomFactorAction(currentPrivacyWebEngineViewPointer->zoomFactor());
1129
1130     // Update the URL.
1131     emit updateWindowTitle(currentPrivacyWebEngineViewPointer->title());
1132     emit updateDomainSettingsIndicator(currentPrivacyWebEngineViewPointer->domainSettingsName != QLatin1String(""));
1133     emit updateUrlLineEdit(currentPrivacyWebEngineViewPointer->url());
1134
1135     // Update the find text.
1136     emit updateFindText(currentPrivacyWebEngineViewPointer->findString, currentPrivacyWebEngineViewPointer->findCaseSensitive);
1137     emit updateFindTextResults(currentPrivacyWebEngineViewPointer->findTextResult);
1138
1139     // Update the progress bar.
1140     if (currentPrivacyWebEngineViewPointer->loadProgressInt >= 0)
1141         emit showProgressBar(currentPrivacyWebEngineViewPointer->loadProgressInt);
1142     else
1143         emit hideProgressBar();
1144 }
1145
1146 void TabWidget::useNativeDownloader(QUrl &downloadUrl, QString &suggestedFileName)
1147 {
1148     // Get the download directory.
1149     QString downloadDirectory = Settings::downloadLocation();
1150
1151     // Resolve the system download directory if specified.
1152     if (downloadDirectory == QLatin1String("System Download Directory"))
1153         downloadDirectory = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
1154
1155     // Create a save file dialog.
1156     QFileDialog *saveFileDialogPointer = new QFileDialog(this, i18nc("Save file dialog caption", "Save File"), downloadDirectory);
1157
1158     // Tell the dialog to use a save button.
1159     saveFileDialogPointer->setAcceptMode(QFileDialog::AcceptSave);
1160
1161     // Populate the file name from the download item pointer.
1162     saveFileDialogPointer->selectFile(suggestedFileName);
1163
1164     // Prevent interaction with the parent window while the dialog is open.
1165     saveFileDialogPointer->setWindowModality(Qt::WindowModal);
1166
1167     // Process the saving of the file.  The save file dialog pointer must be captured directly instead of by reference or nasty crashes occur.
1168     auto saveFile = [saveFileDialogPointer, downloadUrl] ()
1169     {
1170         // Get the save location.  The dialog box should only allow the selecting of one file location.
1171         QUrl saveLocation = saveFileDialogPointer->selectedUrls().value(0);
1172
1173         // Create a file copy job.  `-1` creates the file with default permissions.
1174         KIO::FileCopyJob *fileCopyJobPointer = KIO::file_copy(downloadUrl, saveLocation, -1, KIO::Overwrite);
1175
1176         // Set the download job to display any error messages.
1177         fileCopyJobPointer->uiDelegate()->setAutoErrorHandlingEnabled(true);
1178
1179         // Start the download.
1180         fileCopyJobPointer->start();
1181     };
1182
1183     // Handle clicks on the save button.
1184     connect(saveFileDialogPointer, &QDialog::accepted, this, saveFile);
1185
1186     // Show the dialog.
1187     saveFileDialogPointer->show();
1188 }