]> gitweb.stoutner.com Git - PrivacyBrowserPC.git/blobdiff - src/widgets/TabWidget.cpp
Add a default folder icon to the edit folder dialog. https://redmine.stoutner.com...
[PrivacyBrowserPC.git] / src / widgets / TabWidget.cpp
index ba12f042d4076bc1cc0bb7e679e47f79f7c15e25..f65c79ea2177b404eef157f441245dab7175ed73 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2022 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2022-2024 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser PC <https://www.stoutner.com/privacy-browser-pc>.
  *
  */
 
 // Application headers.
+#include "DevToolsWebEngineView.h"
 #include "TabWidget.h"
 #include "Settings.h"
 #include "ui_AddTabWidget.h"
+#include "ui_Tab.h"
 #include "ui_TabWidget.h"
 #include "databases/CookiesDatabase.h"
-#include "databases/DomainsDatabase.h"
 #include "dialogs/SaveDialog.h"
 #include "filters/MouseEventFilter.h"
 #include "helpers/SearchEngineHelper.h"
-#include "helpers/UserAgentHelper.h"
-#include "interceptors/UrlRequestInterceptor.h"
 #include "windows/BrowserWindow.h"
 
 // KDE Framework headers.
 #include <KIO/FileCopyJob>
 #include <KIO/JobUiDelegate>
+#include <KNotification>
 
 // Qt toolkit headers.
 #include <QAction>
 #include <QFileDialog>
 #include <QGraphicsScene>
 #include <QGraphicsView>
+#include <QMessageBox>
 #include <QPrintDialog>
 #include <QPrintPreviewDialog>
 #include <QPrinter>
 
 // Initialize the public static variables.
-QString TabWidget::webEngineDefaultUserAgent = QStringLiteral("");
+QString TabWidget::webEngineDefaultUserAgent = QLatin1String("");
 
 // Construct the class.
-TabWidget::TabWidget(QWidget *parent) : QWidget(parent)
+TabWidget::TabWidget(QWidget *windowPointer) : QWidget(windowPointer)
 {
+    // Create a QProcess to check if KDE is running.
+    QProcess *checkIfRunningKdeQProcessPointer = new QProcess();
+
+    // Create an argument string list that contains `ksmserver` (KDE Session Manager).
+    QStringList argument = QStringList(QLatin1String("ksmserver"));
+
+    // Run `pidof` to check for the presence of `ksmserver`.
+    checkIfRunningKdeQProcessPointer->start(QLatin1String("pidof"), argument);
+
+    // Monitor any standard output.
+    connect(checkIfRunningKdeQProcessPointer, &QProcess::readyReadStandardOutput, [this]
+    {
+        // If there is any standard output, `ksmserver` is running.
+        isRunningKde = true;
+    });
+
+    // Instantiate the user agent helper.
+    userAgentHelperPointer = new UserAgentHelper();
+
     // Instantiate the UIs.
     Ui::TabWidget tabWidgetUi;
     Ui::AddTabWidget addTabWidgetUi;
@@ -58,25 +78,34 @@ TabWidget::TabWidget(QWidget *parent) : QWidget(parent)
     tabWidgetUi.setupUi(this);
 
     // Get a handle for the tab widget.
-    tabWidgetPointer = tabWidgetUi.tabWidget;
+    qTabWidgetPointer = tabWidgetUi.tabWidget;
 
     // Setup the add tab UI.
-    addTabWidgetUi.setupUi(tabWidgetPointer);
+    addTabWidgetUi.setupUi(qTabWidgetPointer);
 
     // Get handles for the add tab widgets.
     QWidget *addTabWidgetPointer = addTabWidgetUi.addTabQWidget;
     QPushButton *addTabButtonPointer = addTabWidgetUi.addTabButton;
 
     // Display the add tab widget.
-    tabWidgetPointer->setCornerWidget(addTabWidgetPointer);
+    qTabWidgetPointer->setCornerWidget(addTabWidgetPointer);
+
+    // Create the loading favorite icon movie.
+    loadingFavoriteIconMoviePointer = new QMovie();
+
+    // Set the loading favorite icon movie file name.
+    loadingFavoriteIconMoviePointer->setFileName(QStringLiteral(":/icons/loading.gif"));
+
+    // Stop the loading favorite icon movie if the window is destroyed.  Otherwise, the app will crash if there is more than one window open and a window is closed while at tab is loading.
+    connect(windowPointer, SIGNAL(destroyed()), this, SLOT(stopLoadingFavoriteIconMovie()));
 
     // Add the first tab.
     addFirstTab();
 
     // Process tab events.
-    connect(tabWidgetPointer, SIGNAL(currentChanged(int)), this, SLOT(updateUiWithTabSettings()));
+    connect(qTabWidgetPointer, SIGNAL(currentChanged(int)), this, SLOT(updateUiWithTabSettings()));
     connect(addTabButtonPointer, SIGNAL(clicked()), this, SLOT(addTab()));
-    connect(tabWidgetPointer, SIGNAL(tabCloseRequested(int)), this, SLOT(deleteTab(int)));
+    connect(qTabWidgetPointer, SIGNAL(tabCloseRequested(int)), this, SLOT(deleteTab(int)));
 
     // Store a copy of the WebEngine default user agent.
     webEngineDefaultUserAgent = currentWebEngineProfilePointer->httpUserAgent();
@@ -94,32 +123,40 @@ TabWidget::TabWidget(QWidget *parent) : QWidget(parent)
 
 TabWidget::~TabWidget()
 {
+    // Get the number of tabs.
+    int numberOfTabs = qTabWidgetPointer->count();
+
     // Manually delete each WebEngine page.
-    for (int i = 0; i < tabWidgetPointer->count(); ++i)
+    for (int i = 0; i < numberOfTabs; ++i)
     {
-        // Get the privacy WebEngine view.
-        PrivacyWebEngineView *privacyWebEngineViewPointer = qobject_cast<PrivacyWebEngineView *>(tabWidgetPointer->widget(i));
+        // Get the tab splitter widget.
+        QWidget *tabSplitterWidgetPointer = qTabWidgetPointer->widget(i);
+
+        // Get the WebEngine views.
+        PrivacyWebEngineView *privacyWebEngineViewPointer = tabSplitterWidgetPointer->findChild<PrivacyWebEngineView *>();
+        DevToolsWebEngineView *devToolsWebEngineViewPointer = tabSplitterWidgetPointer->findChild<DevToolsWebEngineView *>();
 
-        // Deletion the WebEngine page to prevent the following error:  `Release of profile requested but WebEnginePage still not deleted. Expect troubles !`
+        // Deletion the WebEngine pages to prevent the following error:  `Release of profile requested but WebEnginePage still not deleted. Expect troubles !`
         delete privacyWebEngineViewPointer->page();
+        delete devToolsWebEngineViewPointer->page();
     }
 }
 
 // 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.
 void TabWidget::addCookieToStore(QNetworkCookie cookie, QWebEngineCookieStore *webEngineCookieStorePointer) const
 {
-    // Create a url.
+    // Create a URL.
     QUrl url;
 
     // 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>
-    if (!cookie.domain().startsWith(QStringLiteral(".")))
+    if (!cookie.domain().startsWith(QLatin1String(".")))
     {
         // Populate the URL.
         url.setHost(cookie.domain());
-        url.setScheme(QStringLiteral("https"));
+        url.setScheme(QLatin1String("https"));
 
         // Clear the domain from the cookie.
-        cookie.setDomain(QStringLiteral(""));
+        cookie.setDomain(QLatin1String(""));
     }
 
     // Add the cookie to the store.
@@ -138,40 +175,222 @@ void TabWidget::addFirstTab()
     updateUiWithTabSettings();
 
     // 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.
-    tabWidgetPointer->currentWidget()->setFocus();
+    qTabWidgetPointer->currentWidget()->setFocus();
 }
 
-PrivacyWebEngineView* TabWidget::addTab()
+PrivacyWebEngineView* TabWidget::addTab(const bool removeUrlLineEditFocus, const bool adjacent, const bool backgroundTab, const QString urlString)
 {
-    // Create a privacy WebEngine view.
+    // Create a splitter widget.
+    QSplitter *splitterPointer = new QSplitter();
+
+    // Set the splitter to be vertical.
+    splitterPointer->setOrientation(Qt::Vertical);
+
+    // Set the splitter handle size.
+    splitterPointer->setHandleWidth(5);
+
+    // Create the WebEngines.
     PrivacyWebEngineView *privacyWebEngineViewPointer = new PrivacyWebEngineView();
+    DevToolsWebEngineView *devToolsWebEngineViewPointer = new DevToolsWebEngineView();
+
+    // Add the WebEngines to the splitter.
+    splitterPointer->addWidget(privacyWebEngineViewPointer);
+    splitterPointer->addWidget(devToolsWebEngineViewPointer);
+
+    // Initialize the new tab index.
+    int newTabIndex = 0;
 
     // Add a new tab.
-    int newTabIndex = tabWidgetPointer->addTab(privacyWebEngineViewPointer, i18nc("New tab label.", "New Tab"));
+    if (adjacent)  // Add the new tab adjacent to the current tab.
+        newTabIndex = qTabWidgetPointer->insertTab((qTabWidgetPointer->currentIndex() + 1), splitterPointer, i18nc("New tab label.", "New Tab"));
+    else  // Add the new tab at the end of the list.
+        newTabIndex = qTabWidgetPointer->addTab(splitterPointer, i18nc("New tab label.", "New Tab"));
 
     // Set the default tab icon.
-    tabWidgetPointer->setTabIcon(newTabIndex, defaultTabIcon);
+    qTabWidgetPointer->setTabIcon(newTabIndex, defaultFavoriteIcon);
+
+    // Get handles for the WebEngine components.
+    QWebEnginePage *webEnginePagePointer = privacyWebEngineViewPointer->page();
+    QWebEngineProfile *webEngineProfilePointer = webEnginePagePointer->profile();
+    QWebEngineCookieStore *webEngineCookieStorePointer = webEngineProfilePointer->cookieStore();
+    QWebEngineSettings *webEngineSettingsPointer = webEnginePagePointer->settings();
 
-    // Create an off-the-record profile (the default when no profile name is specified).
-    QWebEngineProfile *webEngineProfilePointer = new QWebEngineProfile(QStringLiteral(""));
+    // Set the development tools WebEngine.  This must be done here to preserve the bottom half of the window as the initial development tools size.
+    webEnginePagePointer->setDevToolsPage(devToolsWebEngineViewPointer->page());
 
-    // Create a WebEngine page.
-    QWebEnginePage *webEnginePagePointer = new QWebEnginePage(webEngineProfilePointer);
+    // Initially hide the development tools WebEngine.
+    devToolsWebEngineViewPointer->setVisible(false);
 
-    // Set the WebEngine page.
-    privacyWebEngineViewPointer->setPage(webEnginePagePointer);
+    // Initially disable the development tools WebEngine.
+    webEnginePagePointer->setDevToolsPage(nullptr);
 
-    // Get handles for the web engine elements.
-    QWebEngineCookieStore *webEngineCookieStorePointer = webEngineProfilePointer->cookieStore();
-    QWebEngineSettings *webEngineSettingsPointer = webEnginePagePointer->settings();
+    // Disable JavaScript on the development tools WebEngine to prevent error messages from being written to the console.
+    devToolsWebEngineViewPointer->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, false);
 
     // Update the URL line edit when the URL changes.
-    connect(privacyWebEngineViewPointer, SIGNAL(urlChanged(const QUrl)), this, SLOT(updateUrl(const QUrl)));
+    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::urlChanged, [this, privacyWebEngineViewPointer] (const QUrl &newUrl)
+    {
+        // Only update the UI if this is the current tab.
+        if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
+        {
+            // Update the URL line edit.
+            emit updateUrlLineEdit(newUrl);
 
-    // Update the progress bar.
-    connect(privacyWebEngineViewPointer, SIGNAL(loadStarted()), this, SLOT(loadStarted()));
-    connect(privacyWebEngineViewPointer, SIGNAL(loadProgress(const int)), this, SLOT(loadProgress(const int)));
-    connect(privacyWebEngineViewPointer, SIGNAL(loadFinished(const bool)), this, SLOT(loadFinished()));
+            // Update the status of the forward and back buttons.
+            emit updateBackAction(currentWebEngineHistoryPointer->canGoBack());
+            emit updateForwardAction(currentWebEngineHistoryPointer->canGoForward());
+        }
+    });
+
+    // Update the title when it changes.
+    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::titleChanged, [this, splitterPointer] (const QString &title)
+    {
+        // Get the index for this tab.
+        int tabIndex = qTabWidgetPointer->indexOf(splitterPointer);
+
+        // Update the title for this tab.
+        qTabWidgetPointer->setTabText(tabIndex, title);
+
+        // Update the window title if this is the current tab.
+        if (tabIndex == qTabWidgetPointer->currentIndex())
+            emit updateWindowTitle(title);
+    });
+
+    // Connect the loading favorite icon movie to the tab icon.
+    connect(loadingFavoriteIconMoviePointer, &QMovie::frameChanged, [this, splitterPointer, privacyWebEngineViewPointer]
+    {
+        // Get the index for this tab.
+        int tabIndex = qTabWidgetPointer->indexOf(splitterPointer);
+
+        // Display the loading favorite icon if this tab is loading.
+        if (privacyWebEngineViewPointer->isLoading)
+            qTabWidgetPointer->setTabIcon(tabIndex, loadingFavoriteIconMoviePointer->currentPixmap());
+    });
+
+    // Update the icon when it changes.
+    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::iconChanged, [this, splitterPointer, privacyWebEngineViewPointer] (const QIcon &newFavoriteIcon)
+    {
+        // Store the favorite icon in the privacy web engine view.
+        if (newFavoriteIcon.isNull())
+            privacyWebEngineViewPointer->favoriteIcon = defaultFavoriteIcon;
+        else
+            privacyWebEngineViewPointer->favoriteIcon = newFavoriteIcon;
+
+        // Get the index for this tab.
+        int tabIndex = qTabWidgetPointer->indexOf(splitterPointer);
+
+        // Update the icon for this tab.
+        if (newFavoriteIcon.isNull())
+            qTabWidgetPointer->setTabIcon(tabIndex, defaultFavoriteIcon);
+        else
+            qTabWidgetPointer->setTabIcon(tabIndex, newFavoriteIcon);
+    });
+
+    // Update the progress bar and the favorite icon when a load is started.
+    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::loadStarted, [this, privacyWebEngineViewPointer] ()
+    {
+        // Set the privacy web engine view to be loading.
+        privacyWebEngineViewPointer->isLoading = true;
+
+        // Store the load progress.
+        privacyWebEngineViewPointer->loadProgressInt = 0;
+
+        // Show the progress bar if this is the current tab.
+        if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
+            emit showProgressBar(0);
+
+        // Start the loading favorite icon movie.
+        loadingFavoriteIconMoviePointer->start();
+    });
+
+    // Update the progress bar when a load progresses.
+    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::loadProgress, [this, privacyWebEngineViewPointer] (const int progress)
+    {
+        // Store the load progress.
+        privacyWebEngineViewPointer->loadProgressInt = progress;
+
+        // Update the progress bar if this is the current tab.
+        if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
+            emit showProgressBar(progress);
+    });
+
+    // Update the progress bar when a load finishes.
+    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::loadFinished, [this, splitterPointer, privacyWebEngineViewPointer] ()
+    {
+        // Set the privacy web engine view to be not loading.
+        privacyWebEngineViewPointer->isLoading = false;
+
+        // Store the load progress.
+        privacyWebEngineViewPointer->loadProgressInt = -1;
+
+        // Hide the progress bar if this is the current tab.
+        if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
+            emit hideProgressBar();
+
+        // Get the index for this tab.
+        int tabIndex = qTabWidgetPointer->indexOf(splitterPointer);
+
+        // Display the current favorite icon
+        qTabWidgetPointer->setTabIcon(tabIndex, privacyWebEngineViewPointer->favoriteIcon);
+
+        // Create a no tabs loading variable.
+        bool noTabsLoading = true;
+
+        // Get the number of tabs.
+        int numberOfTabs = qTabWidgetPointer->count();
+
+        // Check to see if any other tabs are loading.
+        for (int i = 0; i < numberOfTabs; i++)
+        {
+            // Get the privacy WebEngine view for the tab.
+            PrivacyWebEngineView *privacyWebEngineViewPointer = qTabWidgetPointer->widget(i)->findChild<PrivacyWebEngineView *>();
+
+            // Check to see if it is currently loading.  If at least one tab is loading, this flag will end up being marked `false` when the for loop has finished.
+            if (privacyWebEngineViewPointer->isLoading)
+                noTabsLoading = false;
+        }
+
+        // Stop the loading favorite icon movie if there are no loading tabs.
+        if (noTabsLoading)
+            loadingFavoriteIconMoviePointer->stop();
+    });
+
+    // Display HTTP Ping blocked dialogs.
+    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::displayHttpPingBlockedDialog, [this, privacyWebEngineViewPointer] (const QString &httpPingUrl)
+    {
+        // Only display the HTTP Ping blocked dialog if this is the current tab.
+        if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
+        {
+            // Instantiate an HTTP ping blocked message box.
+            QMessageBox httpPingBlockedMessageBox;
+
+            // Set the icon.
+            httpPingBlockedMessageBox.setIcon(QMessageBox::Information);
+
+            // Set the window title.
+            httpPingBlockedMessageBox.setWindowTitle(i18nc("HTTP Ping blocked dialog title", "HTTP Ping Blocked"));
+
+            // Set the text.
+            httpPingBlockedMessageBox.setText(i18nc("HTTP Ping blocked dialog text", "This request has been blocked because it sends a naughty HTTP ping to %1.", httpPingUrl));
+
+            // Set the standard button.
+            httpPingBlockedMessageBox.setStandardButtons(QMessageBox::Ok);
+
+            // Display the message box.
+            httpPingBlockedMessageBox.exec();
+        }
+    });
+
+    // Update the zoom actions when changed by CTRL-Scrolling.  This can be modified when <https://redmine.stoutner.com/issues/845> is fixed.
+    connect(webEnginePagePointer, &QWebEnginePage::contentsSizeChanged, [webEnginePagePointer, this] ()
+    {
+        // Only update the zoom actions if this is the current tab.
+        if (webEnginePagePointer == currentWebEnginePagePointer)
+            emit updateZoomActions(webEnginePagePointer->zoomFactor());
+    });
+
+    // Display find text results.
+    connect(webEnginePagePointer, SIGNAL(findTextFinished(const QWebEngineFindTextResult &)), this, SLOT(findTextFinished(const QWebEngineFindTextResult &)));
 
     // Handle full screen requests.
     connect(webEnginePagePointer, SIGNAL(fullScreenRequested(QWebEngineFullScreenRequest)), this, SLOT(fullScreenRequested(QWebEngineFullScreenRequest)));
@@ -182,15 +401,6 @@ PrivacyWebEngineView* TabWidget::addTab()
     // Handle file downloads.
     connect(webEngineProfilePointer, SIGNAL(downloadRequested(QWebEngineDownloadItem *)), this, SLOT(showSaveDialog(QWebEngineDownloadItem *)));
 
-    // Instantiate the URL request interceptor.
-    UrlRequestInterceptor *urlRequestInterceptorPointer = new UrlRequestInterceptor();
-
-    // Set the URL request interceptor.
-    webEngineProfilePointer->setUrlRequestInterceptor(urlRequestInterceptorPointer);
-
-    // Reapply the domain settings when the host changes.
-    connect(urlRequestInterceptorPointer, SIGNAL(applyDomainSettings(QString)), this, SLOT(applyDomainSettingsWithoutReloading(QString)));
-
     // Set the local storage filter.
     webEngineCookieStorePointer->setCookieFilter([privacyWebEngineViewPointer](const QWebEngineCookieStore::FilterRequest &filterRequest)
     {
@@ -218,14 +428,14 @@ PrivacyWebEngineView* TabWidget::addTab()
         return false;
     });
 
-    // Disable JavaScript by default (this prevetns JavaScript from being enabled on a new tab before domain settings are loaded).
+    // Disable JavaScript by default (this prevents JavaScript from being enabled on a new tab before domain settings are loaded).
     webEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, false);
 
     // Don't allow JavaScript to open windows.
     webEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false);
 
-    // Allow keyboard navigation.
-    webEngineSettingsPointer->setAttribute(QWebEngineSettings::SpatialNavigationEnabled, true);
+    // Allow keyboard navigation between links and input fields.
+    webEngineSettingsPointer->setAttribute(QWebEngineSettings::SpatialNavigationEnabled, Settings::spatialNavigation());
 
     // Enable full screen support.
     webEngineSettingsPointer->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, true);
@@ -236,16 +446,19 @@ PrivacyWebEngineView* TabWidget::addTab()
     // Limit WebRTC to public IP addresses.
     webEngineSettingsPointer->setAttribute(QWebEngineSettings::WebRTCPublicInterfacesOnly, true);
 
-    // Define an update cookie count lambda.
-    auto updateCookieCount = [privacyWebEngineViewPointer, this] (const int numberOfCookies)
+    // Enable the PDF viewer (it should be enabled by default, but it is nice to be explicit in case the defaults change).
+    webEngineSettingsPointer->setAttribute(QWebEngineSettings::PdfViewerEnabled, true);
+
+    // Plugins must be enabled for the PDF viewer to work.  <https://doc.qt.io/qt-5/qtwebengine-features.html#pdf-file-viewing>
+    webEngineSettingsPointer->setAttribute(QWebEngineSettings::PluginsEnabled, true);
+
+    // Update the cookies action.
+    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::updateCookiesAction, [this, privacyWebEngineViewPointer] (const int numberOfCookies)
     {
         // Update the cookie action if the specified privacy WebEngine view is the current privacy WebEngine view.
         if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
             emit updateCookiesAction(numberOfCookies);
-    };
-
-    // Update the cookies action.
-    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::updateCookiesAction, this, updateCookieCount);
+    });
 
     // Process cookie changes.
     connect(webEngineCookieStorePointer, SIGNAL(cookieAdded(QNetworkCookie)), privacyWebEngineViewPointer, SLOT(addCookieToList(QNetworkCookie)));
@@ -258,37 +471,28 @@ PrivacyWebEngineView* TabWidget::addTab()
     for (QNetworkCookie *cookiePointer : *durableCookiesListPointer)
         addCookieToStore(*cookiePointer, webEngineCookieStorePointer);
 
-    // Define an update tab title lambda.
-    auto updateTabTitle = [privacyWebEngineViewPointer, this] (const QString &title)
-    {
-        // Get the index for this tab.
-        int tabIndex = tabWidgetPointer->indexOf(privacyWebEngineViewPointer);
+    // Enable spell checking.
+    webEngineProfilePointer->setSpellCheckEnabled(true);
 
-        // Update the title for this tab.
-        tabWidgetPointer->setTabText(tabIndex, title);
-    };
+    // Set the spell check language.
+    webEngineProfilePointer->setSpellCheckLanguages(Settings::spellCheckLanguages());
 
-    // Update the title when it changes.
-    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::titleChanged, this, updateTabTitle);
+    // Populate the zoom factor.  This is necessary if a URL is being loaded, like a local URL, that does not trigger `applyDomainSettings()`.
+    privacyWebEngineViewPointer->setZoomFactor(Settings::zoomFactor());
 
-    // Define an update tab icon lambda.
-    auto updateTabIcon = [privacyWebEngineViewPointer, this] (const QIcon &icon)
-    {
-        // Get the index for this tab.
-        int tabIndex = tabWidgetPointer->indexOf(privacyWebEngineViewPointer);
+    // Update the UI when domain settings are applied.
+    connect(privacyWebEngineViewPointer, SIGNAL(updateUi(const PrivacyWebEngineView*)), this, SLOT(updateUiFromWebEngineView(const PrivacyWebEngineView*)));
 
-        // Update the icon for this tab.
-        if (icon.isNull())
-            tabWidgetPointer->setTabIcon(tabIndex, defaultTabIcon);
-        else
-            tabWidgetPointer->setTabIcon(tabIndex, icon);
-    };
+    // Move to the new tab if it is not a background tab.
+    if (!backgroundTab)
+        qTabWidgetPointer->setCurrentIndex(newTabIndex);
 
-    // Update the icon when it changes.
-    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::iconChanged, this, updateTabIcon);
+    // Clear the URL line edit focus so that it populates correctly when opening a new tab from the context menu.
+    if (removeUrlLineEditFocus)
+        emit clearUrlLineEditFocus();
 
-    // Move to the new tab.
-    tabWidgetPointer->setCurrentIndex(newTabIndex);
+    if (urlString != nullptr)
+        privacyWebEngineViewPointer->load(QUrl::fromUserInput(urlString));
 
     // Return the privacy WebEngine view pointer.
     return privacyWebEngineViewPointer;
@@ -298,9 +502,21 @@ void TabWidget::applyApplicationSettings()
 {
     // Set the tab position.
     if (Settings::tabsOnTop())
-        tabWidgetPointer->setTabPosition(QTabWidget::North);
+        qTabWidgetPointer->setTabPosition(QTabWidget::North);
     else
-        tabWidgetPointer->setTabPosition(QTabWidget::South);
+        qTabWidgetPointer->setTabPosition(QTabWidget::South);
+
+    // Get the number of tabs.
+    int numberOfTabs = qTabWidgetPointer->count();
+
+    // Apply the spatial navigation settings to each WebEngine.
+    for (int i = 0; i < numberOfTabs; ++i) {
+        // Get the WebEngine view pointer.
+        PrivacyWebEngineView *privacyWebEngineViewPointer = qTabWidgetPointer->widget(i)->findChild<PrivacyWebEngineView *>();
+
+        // Apply the spatial navigation settings to each page.
+        privacyWebEngineViewPointer->page()->settings()->setAttribute(QWebEngineSettings::SpatialNavigationEnabled, Settings::spatialNavigation());
+    }
 
     // Set the search engine URL.
     searchEngineUrl = SearchEngineHelper::getSearchUrl(Settings::searchEngine());
@@ -309,174 +525,19 @@ void TabWidget::applyApplicationSettings()
     emit updateSearchEngineActions(Settings::searchEngine(), true);
 }
 
-// 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.
-// Once <https://redmine.stoutner.com/issues/799> has been resolved this can be `const`.
 void TabWidget::applyDomainSettingsAndReload()
 {
-    // Apply the domain settings.  `true` reloads the website.
-    applyDomainSettings(currentPrivacyWebEngineViewPointer->url().host(), true);
-}
-
-// 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.
-// Once <https://redmine.stoutner.com/issues/799> has been resolved this can be `const`.
-void TabWidget::applyDomainSettingsWithoutReloading(const QString &hostname)
-{
-    // Apply the domain settings  `false` does not reload the website.
-    applyDomainSettings(hostname, false);
-}
-
-// Once <https://redmine.stoutner.com/issues/799> has been resolved this can be `const`.
-void TabWidget::applyDomainSettings(const QString &hostname, const bool reloadWebsite)
-{
-    // Get the record for the hostname.
-    QSqlQuery domainQuery = DomainsDatabase::getDomainQuery(hostname);
-
-    // Check if the hostname has domain settings.
-    if (domainQuery.isValid())  // The hostname has domain settings.
-    {
-        // Get the domain record.
-        QSqlRecord domainRecord = domainQuery.record();
-
-        // Store the domain settings name.
-        currentPrivacyWebEngineViewPointer->domainSettingsName = domainRecord.field(DomainsDatabase::DOMAIN_NAME).value().toString();
-
-        // Set the JavaScript status.
-        switch (domainRecord.field(DomainsDatabase::JAVASCRIPT).value().toInt())
-        {
-            // Set the default JavaScript status.
-            case (DomainsDatabase::SYSTEM_DEFAULT):
-            {
-                currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, Settings::javaScriptEnabled());
-
-                break;
-            }
-
-            // Disable JavaScript.
-            case (DomainsDatabase::DISABLED):
-            {
-                currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, false);
-
-                break;
-            }
-
-            // Enable JavaScript.
-            case (DomainsDatabase::ENABLED):
-            {
-                currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
-
-                break;
-            }
-        }
-
-        // Set the local storage status.
-        switch (domainRecord.field(DomainsDatabase::LOCAL_STORAGE).value().toInt())
-        {
-            // Set the default local storage status.
-            case (DomainsDatabase::SYSTEM_DEFAULT):
-            {
-                currentPrivacyWebEngineViewPointer->localStorageEnabled = Settings::localStorageEnabled();
-
-                break;
-            }
-
-            // Disable local storage.
-            case (DomainsDatabase::DISABLED):
-            {
-                currentPrivacyWebEngineViewPointer->localStorageEnabled = false;
-
-                break;
-            }
-
-            // Enable local storage.
-            case (DomainsDatabase::ENABLED):
-            {
-                currentPrivacyWebEngineViewPointer->localStorageEnabled = true;
-
-                break;
-            }
-        }
-
-        // Set the DOM storage status.
-        switch (domainRecord.field(DomainsDatabase::DOM_STORAGE).value().toInt())
-        {
-            // Set the default DOM storage status.
-            case (DomainsDatabase::SYSTEM_DEFAULT):
-            {
-                currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, Settings::domStorageEnabled());
-
-                break;
-            }
+    // Get the number of tabs.
+    int numberOfTabs = qTabWidgetPointer->count();
 
-            // Disable DOM storage.
-            case (DomainsDatabase::DISABLED):
-            {
-                currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, false);
-
-                break;
-            }
+    // Apply the domain settings to each WebEngine.
+    for (int i = 0; i < numberOfTabs; ++i) {
+        // Get the WebEngine view pointer.
+        PrivacyWebEngineView *privacyWebEngineViewPointer = qTabWidgetPointer->widget(i)->findChild<PrivacyWebEngineView *>();
 
-            // Enable DOM storage.
-            case (DomainsDatabase::ENABLED):
-            {
-                currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, true);
-
-                break;
-            }
-        }
-
-        // Set the user agent.
-        currentWebEngineProfilePointer->setHttpUserAgent(UserAgentHelper::getResultingDomainSettingsUserAgent(domainRecord.field(DomainsDatabase::USER_AGENT).value().toString()));
-
-        // Check if a custom zoom factor is set.
-        if (domainRecord.field(DomainsDatabase::ZOOM_FACTOR).value().toInt())
-        {
-            // Store the current zoom factor.
-            currentZoomFactor = domainRecord.field(DomainsDatabase::CUSTOM_ZOOM_FACTOR).value().toDouble();
-        }
-        else
-        {
-            // Reset the current zoom factor.
-            currentZoomFactor = Settings::zoomFactor();
-        }
-
-        // Set the zoom factor.    The use of `currentZoomFactor` can be removed once <https://redmine.stoutner.com/issues/799> has been resolved.
-        currentPrivacyWebEngineViewPointer->setZoomFactor(currentZoomFactor);
+        // Apply the spatial navigation settings to each page.
+        privacyWebEngineViewPointer->applyDomainSettings(privacyWebEngineViewPointer->url().host(), true);
     }
-    else  // The hostname does not have domain settings.
-    {
-        // Reset the domain settings name.
-        currentPrivacyWebEngineViewPointer->domainSettingsName = QStringLiteral("");
-
-        // Set the JavaScript status.
-        currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, Settings::javaScriptEnabled());
-
-        // Set the local storage status.
-        currentPrivacyWebEngineViewPointer->localStorageEnabled = Settings::localStorageEnabled();
-
-        // Set DOM storage.  In QWebEngineSettings it is called Local Storage.
-        currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, Settings::domStorageEnabled());
-
-        // Set the user agent.
-        currentWebEngineProfilePointer->setHttpUserAgent(UserAgentHelper::getUserAgentFromDatabaseName(Settings::userAgent()));
-
-        // Store the current zoom factor.  This can be removed once <https://redmine.stoutner.com/issues/799> has been resolved.
-        currentZoomFactor = Settings::zoomFactor();
-
-        // Set the zoom factor.
-        currentPrivacyWebEngineViewPointer->setZoomFactor(Settings::zoomFactor());
-    }
-
-    // Update the UI.
-    emit updateDomainSettingsIndicator(currentPrivacyWebEngineViewPointer->domainSettingsName != QStringLiteral(""));
-    emit updateJavaScriptAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::JavascriptEnabled));
-    emit updateLocalStorageAction(currentPrivacyWebEngineViewPointer->localStorageEnabled);
-    emit updateDomStorageAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::LocalStorageEnabled));
-    emit updateUserAgentActions(currentWebEngineProfilePointer->httpUserAgent(), true);
-    emit updateZoomFactorAction(currentPrivacyWebEngineViewPointer->zoomFactor());
-
-    // Reload the website if requested.
-    if (reloadWebsite)
-        currentPrivacyWebEngineViewPointer->reload();
 }
 
 void TabWidget::applyOnTheFlySearchEngine(QAction *searchEngineActionPointer)
@@ -490,7 +551,7 @@ void TabWidget::applyOnTheFlySearchEngine(QAction *searchEngineActionPointer)
     // Store the search engine string.
     searchEngineUrl = SearchEngineHelper::getSearchUrl(searchEngineName);
 
-    // Update the search engine actionas.
+    // Update the search engine actions.
     emit updateSearchEngineActions(searchEngineName, false);
 }
 
@@ -503,7 +564,7 @@ void TabWidget::applyOnTheFlyUserAgent(QAction *userAgentActionPointer) const
     userAgentName.remove('&');
 
     // Apply the user agent.
-    currentWebEngineProfilePointer->setHttpUserAgent(UserAgentHelper::getUserAgentFromTranslatedName(userAgentName));
+    currentWebEngineProfilePointer->setHttpUserAgent(userAgentHelperPointer->getUserAgentFromTranslatedName(userAgentName));
 
     // Update the user agent actions.
     emit updateUserAgentActions(currentWebEngineProfilePointer->httpUserAgent(), false);
@@ -512,14 +573,32 @@ void TabWidget::applyOnTheFlyUserAgent(QAction *userAgentActionPointer) const
     currentPrivacyWebEngineViewPointer->reload();
 }
 
-// This can be const once <https://redmine.stoutner.com/issues/799> has been resolved.
-void TabWidget::applyOnTheFlyZoomFactor(const double &zoomFactor)
+void TabWidget::applyOnTheFlyZoomFactor(const double zoomFactorDouble) const
 {
-    // Update the current zoom factor.  This can be removed once <https://redmine.stoutner.com/issues/799> has been resolved.
-    currentZoomFactor = zoomFactor;
-
     // Set the zoom factor.
-    currentPrivacyWebEngineViewPointer->setZoomFactor(zoomFactor);
+    currentPrivacyWebEngineViewPointer->setZoomFactor(zoomFactorDouble);
+}
+
+void TabWidget::applySpellCheckLanguages() const
+{
+    // Get the number of tab.
+    int numberOfTabs = qTabWidgetPointer->count();
+
+    // Set the spell check languages for each tab.
+    for (int i = 0; i < numberOfTabs; ++i)
+    {
+        // Get the WebEngine view pointer.
+        PrivacyWebEngineView *privacyWebEngineViewPointer = qTabWidgetPointer->widget(i)->findChild<PrivacyWebEngineView *>();
+
+        // Get the WebEngine page pointer.
+        QWebEnginePage *webEnginePagePointer = privacyWebEngineViewPointer->page();
+
+        // Get the WebEngine profile pointer.
+        QWebEngineProfile *webEngineProfilePointer = webEnginePagePointer->profile();
+
+        // Set the spell check languages.
+        webEngineProfilePointer->setSpellCheckLanguages(Settings::spellCheckLanguages());
+    }
 }
 
 void TabWidget::back() const
@@ -542,20 +621,29 @@ void TabWidget::deleteCookieFromStore(const QNetworkCookie &cookie) const
 
 void TabWidget::deleteTab(const int tabIndex)
 {
-    // Get the privacy WebEngine view.
-    PrivacyWebEngineView *privacyWebEngineViewPointer = qobject_cast<PrivacyWebEngineView *>(tabWidgetPointer->widget(tabIndex));
+    // Get the tab splitter widget.
+    QWidget *tabSplitterWidgetPointer = qTabWidgetPointer->widget(tabIndex);
+
+    // Get the WebEngine views.
+    PrivacyWebEngineView *privacyWebEngineViewPointer = tabSplitterWidgetPointer->findChild<PrivacyWebEngineView *>();
+    DevToolsWebEngineView *devToolsWebEngineViewPointer = tabSplitterWidgetPointer->findChild<DevToolsWebEngineView *>();
 
-    // Proccess the tab delete according to the number of tabs.
-    if (tabWidgetPointer->count() > 1)  // There is more than one tab.
+    // Process the tab delete according to the number of tabs.
+    if (qTabWidgetPointer->count() > 1)  // There is more than one tab.
     {
-        // Delete the tab.
-        tabWidgetPointer->removeTab(tabIndex);
+        // Remove the tab.
+        qTabWidgetPointer->removeTab(tabIndex);
 
-        // Delete the WebEngine page to prevent the following error:  `Release of profile requested but WebEnginePage still not deleted. Expect troubles !`
+        // Delete the WebEngine pages to prevent the following error:  `Release of profile requested but WebEnginePage still not deleted. Expect troubles !`
         delete privacyWebEngineViewPointer->page();
+        delete devToolsWebEngineViewPointer->page();
 
-        // Delete the privacy WebEngine view.
+        // Delete the WebEngine views.
         delete privacyWebEngineViewPointer;
+        delete devToolsWebEngineViewPointer;
+
+        // Delete the tab splitter widget.
+        delete tabSplitterWidgetPointer;
     }
     else  // There is only one tab.
     {
@@ -564,6 +652,52 @@ void TabWidget::deleteTab(const int tabIndex)
     }
 }
 
+void TabWidget::findPrevious(const QString &text) const
+{
+    // Store the current text.
+    currentPrivacyWebEngineViewPointer->findString = text;
+
+    // Find the previous text in the current privacy WebEngine.
+    if (currentPrivacyWebEngineViewPointer->findCaseSensitive)
+        currentPrivacyWebEngineViewPointer->findText(text, QWebEnginePage::FindCaseSensitively|QWebEnginePage::FindBackward);
+    else
+        currentPrivacyWebEngineViewPointer->findText(text, QWebEnginePage::FindBackward);
+}
+
+void TabWidget::findText(const QString &text) const
+{
+    // Store the current text.
+    currentPrivacyWebEngineViewPointer->findString = text;
+
+    // Find the text in the current privacy WebEngine.
+    if (currentPrivacyWebEngineViewPointer->findCaseSensitive)
+        currentPrivacyWebEngineViewPointer->findText(text, QWebEnginePage::FindCaseSensitively);
+    else
+        currentPrivacyWebEngineViewPointer->findText(text);
+
+    // Clear the currently selected text in the WebEngine page if the find text is empty.
+    if (text.isEmpty())
+        currentWebEnginePagePointer->action(QWebEnginePage::Unselect)->activate(QAction::Trigger);
+}
+
+void TabWidget::findTextFinished(const QWebEngineFindTextResult &findTextResult)
+{
+    // Update the find text UI if it wasn't simply wiping the current find text selection.  Otherwise the UI temporarily flashes `0/0`.
+    if (wipingCurrentFindTextSelection)  // The current selection is being wiped.
+    {
+        // Reset the flag.
+        wipingCurrentFindTextSelection = false;
+    }
+    else  // A new search has been performed.
+    {
+        // Store the result.
+        currentPrivacyWebEngineViewPointer->findTextResult = findTextResult;
+
+        // Update the UI.
+        emit updateFindTextResults(findTextResult);
+    }
+}
+
 void TabWidget::forward() const
 {
     // Go forward.
@@ -585,6 +719,30 @@ std::list<QNetworkCookie>* TabWidget::getCookieList() const
     return currentPrivacyWebEngineViewPointer->cookieListPointer;
 }
 
+QIcon TabWidget::getCurrentTabFavoritIcon() const
+{
+    // Return the current Privacy WebEngine favorite icon.
+    return currentPrivacyWebEngineViewPointer->favoriteIcon;
+}
+
+QString TabWidget::getCurrentTabTitle() const
+{
+    // Return the current Privacy WebEngine title.
+    return currentPrivacyWebEngineViewPointer->title();
+}
+
+QString TabWidget::getCurrentTabUrl() const
+{
+    // Return the current Privacy WebEngine URL as a string.
+    return currentPrivacyWebEngineViewPointer->url().toString();
+}
+
+QString TabWidget::getCurrentUserAgent() const
+{
+    // Return the current WebEngine user agent.
+    return currentWebEngineProfilePointer->httpUserAgent();
+}
+
 QString& TabWidget::getDomainSettingsName() const
 {
     // Return the domain settings name.
@@ -597,10 +755,13 @@ void TabWidget::home() const
     currentPrivacyWebEngineViewPointer->load(QUrl::fromUserInput(Settings::homepage()));
 }
 
-void TabWidget::loadFinished() const
+PrivacyWebEngineView* TabWidget::loadBlankInitialWebsite()
 {
-    // Hide the progress bar.
-    emit hideProgressBar();
+    // Apply the application settings.
+    applyApplicationSettings();
+
+    // Return the current privacy WebEngine view pointer.
+    return currentPrivacyWebEngineViewPointer;
 }
 
 void TabWidget::loadInitialWebsite()
@@ -624,22 +785,10 @@ void TabWidget::loadInitialWebsite()
     }
 }
 
-void TabWidget::loadProgress(const int &progress) const
-{
-    // Show the progress bar.
-    emit showProgressBar(progress);
-}
-
-void TabWidget::loadStarted() const
-{
-    // Show the progress bar.
-    emit showProgressBar(0);
-}
-
 void TabWidget::loadUrlFromLineEdit(QString url) const
 {
     // Decide if the text is more likely to be a URL or a search.
-    if (url.startsWith("file://"))  // The text is likely a file URL.
+    if (url.startsWith("file://") || url.startsWith("view-source:"))  // The text is likely a file or view source URL.
     {
         // Load the URL.
         currentPrivacyWebEngineViewPointer->load(QUrl::fromUserInput(url));
@@ -695,6 +844,12 @@ void TabWidget::pageLinkHovered(const QString &linkUrl) const
     emit linkHovered(linkUrl);
 }
 
+void TabWidget::stopLoadingFavoriteIconMovie() const
+{
+    // Stop the loading favorite icon movie.  Otherwise, the browser will crash if a second window is closed while a tab in it is loading.  <https://redmine.stoutner.com/issues/1010>
+    loadingFavoriteIconMoviePointer->stop();
+}
+
 void TabWidget::print() const
 {
     // Create a printer.
@@ -751,69 +906,263 @@ void TabWidget::printWebpage(QPrinter *printerPointer) const
 
 void TabWidget::refresh() const
 {
+    // Reset the HTTP authentication dialog counter.
+    currentPrivacyWebEngineViewPointer->httpAuthenticationDialogsDisplayed = 0;
+
     // Reload the website.
     currentPrivacyWebEngineViewPointer->reload();
 }
 
-void TabWidget::setTabBarVisible(const bool visible) const
+void TabWidget::reloadAndBypassCache() const
 {
-    // Set the tab bar visibility.
-    tabWidgetPointer->tabBar()->setVisible(visible);
+    // Reload the website, bypassing the cache.
+    currentWebEnginePagePointer->triggerAction(QWebEnginePage::ReloadAndBypassCache);
 }
 
-void TabWidget::showSaveDialog(QWebEngineDownloadItem *downloadItemPointer) const
+void TabWidget::saveArchive()
 {
-    // Instantiate the save dialog.
-    SaveDialog *saveDialogPointer = new SaveDialog(downloadItemPointer);
+    // Get the suggested file name.
+    QString suggestedFileName = currentPrivacyWebEngineViewPointer->title() + ".mht";
 
-    // Connect the save button.
-    connect(saveDialogPointer, SIGNAL(showSaveFilePickerDialog(QUrl &, QString &)), this, SLOT(showSaveFilePickerDialog(QUrl &, QString &)));
+    // Get the download directory.
+    QString downloadDirectory = Settings::downloadDirectory();
 
-    // Show the dialog.
-    saveDialogPointer->show();
+    // Resolve the system download directory if specified.
+    if (downloadDirectory == QLatin1String("System Download Directory"))
+        downloadDirectory = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
+
+    // Get a file path from the file picker.
+    QString saveFilePath = QFileDialog::getSaveFileName(this, i18nc("Save file dialog caption", "Save File"), downloadDirectory + QLatin1Char('/') + suggestedFileName);
+
+    // Save the webpage as an archive if the file save path is populated.
+    if (!saveFilePath.isEmpty())
+    {
+        // Update the download directory if specified.
+        if (Settings::autoUpateDownloadDirectory())
+            updateDownloadDirectory(saveFilePath);
+
+        // Set the saving archive flag.  Otherwise, a second download tries to run.
+        savingArchive = true;
+
+        // Save the archive.
+        currentWebEnginePagePointer->save(saveFilePath);
+    }
 }
 
-void TabWidget::showSaveFilePickerDialog(QUrl &downloadUrl, QString &suggestedFileName)
+void TabWidget::setTabBarVisible(const bool visible) const
 {
-    // Get the download location.
-    QString downloadDirectory = Settings::downloadLocation();
+    // Set the tab bar visibility.
+    qTabWidgetPointer->tabBar()->setVisible(visible);
+}
 
-    // Resolve the system download directory if specified.
-    if (downloadDirectory == QStringLiteral("System Download Directory"))
-        downloadDirectory = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
+void TabWidget::showSaveDialog(QWebEngineDownloadItem *webEngineDownloadItemPointer)
+{
+    // Only show the save dialog if an archive is not currently being saved.  Otherwise, two save dialogs will be shown.
+    if (!savingArchive)
+    {
+        // Get the download attributes.
+        QUrl downloadUrl = webEngineDownloadItemPointer->url();
+        QString mimeTypeString = webEngineDownloadItemPointer->mimeType();
+        QString suggestedFileName = webEngineDownloadItemPointer->suggestedFileName();
+        int totalBytes = webEngineDownloadItemPointer->totalBytes();
+
+        // Check to see if Privacy Browser is not running KDE or if local storage (cookies) is enabled.
+        if (!isRunningKde || currentPrivacyWebEngineViewPointer->localStorageEnabled)  // KDE is not running or local storage (cookies) is enabled.  Use WebEngine's downloader.
+        {
+            // Instantiate the save dialog.
+            SaveDialog *saveDialogPointer = new SaveDialog(this, downloadUrl, mimeTypeString, totalBytes);
 
-    // Create a save file dialog.
-    QFileDialog *saveFileDialogPointer = new QFileDialog(this, i18nc("Save file dialog caption", "Save File"), downloadDirectory);
+            // Display the save dialog.
+            int saveDialogResult = saveDialogPointer->exec();
 
-    // Tell the dialog to use a save button.
-    saveFileDialogPointer->setAcceptMode(QFileDialog::AcceptSave);
+            // Process the save dialog results.
+            if (saveDialogResult == QDialog::Accepted)  // Save was selected.
+            {
+                // Get the download directory.
+                QString downloadDirectory = Settings::downloadDirectory();
 
-    // Populate the file name from the download item pointer.
-    saveFileDialogPointer->selectFile(suggestedFileName);
+                // Resolve the system download directory if specified.
+                if (downloadDirectory == QLatin1String("System Download Directory"))
+                    downloadDirectory = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
 
-    // Prevent interaction with the parent window while the dialog is open.
-    saveFileDialogPointer->setWindowModality(Qt::WindowModal);
+                // Get a file path from the file picker.
+                QString saveFilePath = QFileDialog::getSaveFileName(this, i18nc("Save file dialog caption", "Save File"), downloadDirectory + QLatin1Char('/') + suggestedFileName);
 
-    // Process the saving of the file.  The save file dialog pointer must be captured directly instead of by reference or nasty crashes occur.
-    auto saveFile = [saveFileDialogPointer, &downloadUrl] () {
-        // Get the save location.  The dialog box should only allow the selecting of one file location.
-        QUrl saveLocation = saveFileDialogPointer->selectedUrls().value(0);
+                // Process the save file path.
+                if (!saveFilePath.isEmpty())  // The file save path is populated.
+                {
+                    // Update the download directory if specified.
+                    if (Settings::autoUpateDownloadDirectory())
+                        updateDownloadDirectory(saveFilePath);
 
-        // Create a file copy job.  `-1` creates the file with default permissions.
-        KIO::FileCopyJob *fileCopyJobPointer = KIO::file_copy(downloadUrl, saveLocation, -1, KIO::Overwrite);
+                    // Create a save file path file info.
+                    QFileInfo saveFilePathFileInfo = QFileInfo(saveFilePath);
+
+                    // Get the canonical save path and file name.
+                    QString absoluteSavePath = saveFilePathFileInfo.absolutePath();
+                    QString saveFileName = saveFilePathFileInfo.fileName();
+
+                    // Set the download directory and file name.
+                    webEngineDownloadItemPointer->setDownloadDirectory(absoluteSavePath);
+                    webEngineDownloadItemPointer->setDownloadFileName(saveFileName);
+
+                    // Create a file download notification.
+                    KNotification *fileDownloadNotificationPointer = new KNotification(QLatin1String("FileDownload"));
+
+                    // Set the notification title.
+                    fileDownloadNotificationPointer->setTitle(i18nc("Download notification title", "Download"));
+
+                    // Set the notification text.
+                    fileDownloadNotificationPointer->setText(i18nc("Downloading notification text", "Downloading %1", saveFileName));
+
+                    // Get the download icon from the theme.
+                    QIcon downloadIcon = QIcon::fromTheme(QLatin1String("download"), QIcon::fromTheme(QLatin1String("document-save")));
+
+                    // Set the notification icon.
+                    fileDownloadNotificationPointer->setIconName(downloadIcon.name());
+
+                    // Set the action list cancel button.
+                    fileDownloadNotificationPointer->setActions(QStringList({i18nc("Download notification action","Cancel")}));
+
+                    // Prevent the notification from being autodeleted if it is closed.  Otherwise, the updates to the notification below cause a crash.
+                    fileDownloadNotificationPointer->setAutoDelete(false);
+
+                    // Handle clicks on the cancel button.
+                    connect(fileDownloadNotificationPointer, &KNotification::action1Activated, [webEngineDownloadItemPointer, saveFileName] ()
+                    {
+                        // Cancel the download.
+                        webEngineDownloadItemPointer->cancel();
+
+                        // Create a file download notification.
+                        KNotification *canceledDownloadNotificationPointer = new KNotification(QLatin1String("FileDownload"));
+
+                        // Set the notification title.
+                        canceledDownloadNotificationPointer->setTitle(i18nc("Download notification title", "Download"));
+
+                        // Set the new text.
+                        canceledDownloadNotificationPointer->setText(i18nc("Download canceled notification", "%1 download canceled", saveFileName));
+
+                        // Set the notification icon.
+                        canceledDownloadNotificationPointer->setIconName(QLatin1String("download"));
+
+                        // Display the notification.
+                        canceledDownloadNotificationPointer->sendEvent();
+                    });
+
+                    // Update the notification when the download progresses.
+                    connect(webEngineDownloadItemPointer, &QWebEngineDownloadItem::downloadProgress, [fileDownloadNotificationPointer, saveFileName] (qint64 bytesReceived, qint64 totalBytes)
+                    {
+                        // Set the new text.  Total bytes will be 0 if the download size is unknown.
+                        if (totalBytes > 0)
+                        {
+                            // Calculate the download percentage.
+                            int downloadPercentage = 100 * bytesReceived / totalBytes;
+
+                            // Set the file download notification text.
+                            fileDownloadNotificationPointer->setText(i18nc("Download progress notification text", "%1\% of %2 downloaded (%3 of %4 bytes)", downloadPercentage, saveFileName,
+                                                                        bytesReceived, totalBytes));
+                        }
+                        else
+                        {
+                            // Set the file download notification text.
+                            fileDownloadNotificationPointer->setText(i18nc("Download progress notification text", "%1:  %2 bytes downloaded", saveFileName, bytesReceived));
+                        }
+
+                        // Display the updated notification.
+                        fileDownloadNotificationPointer->update();
+                    });
+
+                    // Update the notification when the download finishes.  The save file name must be copied into the lambda or a crash occurs.
+                    connect(webEngineDownloadItemPointer, &QWebEngineDownloadItem::finished, [fileDownloadNotificationPointer, saveFileName, saveFilePath] ()
+                    {
+                        // Set the new text.
+                        fileDownloadNotificationPointer->setText(i18nc("Download finished notification text", "%1 download finished", saveFileName));
+
+                        // Set the URL so the file options will be displayed.
+                        fileDownloadNotificationPointer->setUrls(QList<QUrl> {QUrl(saveFilePath)});
+
+                        // Remove the actions from the notification.
+                        fileDownloadNotificationPointer->setActions(QStringList());
+
+                        // Set the notification to disappear after a timeout.
+                        fileDownloadNotificationPointer->setFlags(KNotification::CloseOnTimeout);
+
+                        // Display the updated notification.
+                        fileDownloadNotificationPointer->update();
+                    });
+
+                    // Display the notification.
+                    fileDownloadNotificationPointer->sendEvent();
+
+                    // Start the download.
+                    webEngineDownloadItemPointer->accept();
+                }
+                else  // The file save path is not populated.
+                {
+                    // Cancel the download.
+                    webEngineDownloadItemPointer->cancel();
+                }
+            }
+            else  // Cancel was selected.
+            {
+                // Cancel the download.
+                webEngineDownloadItemPointer->cancel();
+            }
+        }
+        else  // KDE is running and local storage (cookies) is disabled.  Use KDE's native downloader.
+            // 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.
+        {
+            // Instantiate the save dialog.  `true` instructs it to use the native downloader
+            SaveDialog *saveDialogPointer = new SaveDialog(this, downloadUrl, mimeTypeString, totalBytes, suggestedFileName, true);
 
-        // Set the download job to display any error messages.
-        fileCopyJobPointer->uiDelegate()->setAutoErrorHandlingEnabled(true);
+            // Connect the save button.
+            connect(saveDialogPointer, SIGNAL(useNativeKdeDownloader(QUrl &, QString &)), this, SLOT(useNativeKdeDownloader(QUrl &, QString &)));
 
-        // Start the download.
-        fileCopyJobPointer->start();
-    };
+            // Show the dialog.
+            saveDialogPointer->show();
+        }
+    }
 
-    // Handle clicks on the save button.
-    connect(saveFileDialogPointer, &QDialog::accepted, this, saveFile);
+    // Reset the saving archive flag.
+    savingArchive = false;
+}
 
-    // Show the dialog.
-    saveFileDialogPointer->show();
+void TabWidget::stop() const
+{
+    // Stop the loading of the current privacy WebEngine.
+    currentPrivacyWebEngineViewPointer->stop();
+}
+
+void TabWidget::toggleDeveloperTools(const bool enabled) const
+{
+    // Get a handle for the current developer tools WebEngine.
+    DevToolsWebEngineView *devToolsWebEngineViewPointer = qTabWidgetPointer->currentWidget()->findChild<DevToolsWebEngineView *>();
+
+    if (enabled)
+    {
+        // Set the zoom factor on the development tools WebEngine.
+        devToolsWebEngineViewPointer->setZoomFactor(currentWebEnginePagePointer->zoomFactor());
+
+        // Enable the development tools.
+        currentWebEnginePagePointer->setDevToolsPage(devToolsWebEngineViewPointer->page());
+
+        // Enable JavaScript on the development tools WebEngine.
+        devToolsWebEngineViewPointer->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
+
+        // Display the developer tools.
+        devToolsWebEngineViewPointer->setVisible(true);
+    }
+    else
+    {
+        // Disable JavaScript on the development tools WebEngine to prevent error messages from being written to the console.
+        devToolsWebEngineViewPointer->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, false);
+
+        // Disable the development tools.
+        currentWebEnginePagePointer->setDevToolsPage(nullptr);
+
+        // Hide the developer tools.
+        devToolsWebEngineViewPointer->setVisible(false);
+    }
 }
 
 void TabWidget::toggleDomStorage() const
@@ -828,6 +1177,21 @@ void TabWidget::toggleDomStorage() const
     currentPrivacyWebEngineViewPointer->reload();
 }
 
+void TabWidget::toggleFindCaseSensitive(const QString &text)
+{
+    // Toggle find case sensitive.
+    currentPrivacyWebEngineViewPointer->findCaseSensitive = !currentPrivacyWebEngineViewPointer->findCaseSensitive;
+
+    // Set the wiping current find text selection flag.
+    wipingCurrentFindTextSelection = true;
+
+    // Wipe the previous search.  Otherwise currently highlighted words will remain highlighted.
+    findText(QLatin1String(""));
+
+    // Update the find text.
+    findText(text);
+}
+
 void TabWidget::toggleJavaScript() const
 {
     // Toggle JavaScript.
@@ -842,7 +1206,7 @@ void TabWidget::toggleJavaScript() const
 
 void TabWidget::toggleLocalStorage()
 {
-    // Toggle local storeage.
+    // Toggle local storage.
     currentPrivacyWebEngineViewPointer->localStorageEnabled = !currentPrivacyWebEngineViewPointer->localStorageEnabled;
 
     // Update the local storage action.
@@ -852,10 +1216,41 @@ void TabWidget::toggleLocalStorage()
     currentPrivacyWebEngineViewPointer->reload();
 }
 
+void TabWidget::updateDownloadDirectory(QString newDownloadDirectory) const
+{
+    // Remove the file name from the save file path.
+    newDownloadDirectory.truncate(newDownloadDirectory.lastIndexOf(QLatin1Char('/')));
+
+    // Update the download location.
+    Settings::setDownloadDirectory(newDownloadDirectory);
+
+    // Get a handle for the KConfig skeleton.
+    KConfigSkeleton *kConfigSkeletonPointer = Settings::self();
+
+    // Write the settings to disk.
+    kConfigSkeletonPointer->save();
+}
+
+void TabWidget::updateUiFromWebEngineView(const PrivacyWebEngineView *privacyWebEngineViewPointer) const
+{
+    // Only update the UI if the signal was emitted from the current privacy WebEngine.
+    if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
+    {
+        // Update the UI.
+        emit updateDefaultZoomFactor(currentPrivacyWebEngineViewPointer->defaultZoomFactor);
+        emit updateDomainSettingsIndicator(currentPrivacyWebEngineViewPointer->domainSettingsName != QLatin1String(""));
+        emit updateJavaScriptAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::JavascriptEnabled));
+        emit updateLocalStorageAction(currentPrivacyWebEngineViewPointer->localStorageEnabled);
+        emit updateDomStorageAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::LocalStorageEnabled));
+        emit updateUserAgentActions(currentWebEngineProfilePointer->httpUserAgent(), true);
+        emit updateZoomActions(currentPrivacyWebEngineViewPointer->zoomFactor());
+    }
+}
+
 void TabWidget::updateUiWithTabSettings()
 {
     // Update the current WebEngine pointers.
-    currentPrivacyWebEngineViewPointer = qobject_cast<PrivacyWebEngineView *>(tabWidgetPointer->currentWidget());
+    currentPrivacyWebEngineViewPointer = qTabWidgetPointer->currentWidget()->findChild<PrivacyWebEngineView *>();
     currentWebEngineSettingsPointer = currentPrivacyWebEngineViewPointer->settings();
     currentWebEnginePagePointer = currentPrivacyWebEngineViewPointer->page();
     currentWebEngineProfilePointer = currentWebEnginePagePointer->profile();
@@ -865,26 +1260,82 @@ void TabWidget::updateUiWithTabSettings()
     // Clear the URL line edit focus.
     emit clearUrlLineEditFocus();
 
-    // Update the UI.
-    emit updateDomainSettingsIndicator(currentPrivacyWebEngineViewPointer->domainSettingsName != QStringLiteral(""));
+    // Get a handle for the development tools WebEngine view.
+    DevToolsWebEngineView *devToolsWebEngineViewPointer = qTabWidgetPointer->currentWidget()->findChild<DevToolsWebEngineView *>();
+
+    // Update the actions.
+    emit updateDefaultZoomFactor(currentPrivacyWebEngineViewPointer->defaultZoomFactor);
+    emit updateBackAction(currentWebEngineHistoryPointer->canGoBack());
+    emit updateCookiesAction(currentPrivacyWebEngineViewPointer->cookieListPointer->size());
+    emit updateDeveloperToolsAction(devToolsWebEngineViewPointer->isVisible());
+    emit updateDomStorageAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::LocalStorageEnabled));
+    emit updateForwardAction(currentWebEngineHistoryPointer->canGoForward());
     emit updateJavaScriptAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::JavascriptEnabled));
     emit updateLocalStorageAction(currentPrivacyWebEngineViewPointer->localStorageEnabled);
-    emit updateDomStorageAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::LocalStorageEnabled));
     emit updateUserAgentActions(currentWebEngineProfilePointer->httpUserAgent(), true);
-    emit updateZoomFactorAction(currentPrivacyWebEngineViewPointer->zoomFactor());
+    emit updateZoomActions(currentPrivacyWebEngineViewPointer->zoomFactor());
+
+    // Update the URL.
+    emit updateWindowTitle(currentPrivacyWebEngineViewPointer->title());
+    emit updateDomainSettingsIndicator(currentPrivacyWebEngineViewPointer->domainSettingsName != QLatin1String(""));
     emit updateUrlLineEdit(currentPrivacyWebEngineViewPointer->url());
-    emit updateCookiesAction(currentPrivacyWebEngineViewPointer->cookieListPointer->size());
+
+    // Update the find text.
+    emit updateFindText(currentPrivacyWebEngineViewPointer->findString, currentPrivacyWebEngineViewPointer->findCaseSensitive);
+    emit updateFindTextResults(currentPrivacyWebEngineViewPointer->findTextResult);
+
+    // Update the progress bar.
+    if (currentPrivacyWebEngineViewPointer->loadProgressInt >= 0)
+        emit showProgressBar(currentPrivacyWebEngineViewPointer->loadProgressInt);
+    else
+        emit hideProgressBar();
 }
 
-void TabWidget::updateUrl(const QUrl &url) const
+void TabWidget::useNativeKdeDownloader(QUrl &downloadUrl, QString &suggestedFileName)
 {
-    // Update the URL line edit.
-    emit updateUrlLineEdit(url);
+    // Get the download directory.
+    QString downloadDirectory = Settings::downloadDirectory();
 
-    // Update the status of the forward and back buttons.
-    emit updateBackAction(currentWebEngineHistoryPointer->canGoBack());
-    emit updateForwardAction(currentWebEngineHistoryPointer->canGoForward());
+    // Resolve the system download directory if specified.
+    if (downloadDirectory == QLatin1String("System Download Directory"))
+        downloadDirectory = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
+
+    // Create a save file dialog.
+    QFileDialog *saveFileDialogPointer = new QFileDialog(this, i18nc("Save file dialog caption", "Save File"), downloadDirectory);
+
+    // Tell the dialog to use a save button.
+    saveFileDialogPointer->setAcceptMode(QFileDialog::AcceptSave);
+
+    // Populate the file name from the download item pointer.
+    saveFileDialogPointer->selectFile(suggestedFileName);
 
-    // Reapply the zoom factor.  This is a bug in QWebEngineView that resets the zoom with every load.  <https://redmine.stoutner.com/issues/799>
-    currentPrivacyWebEngineViewPointer->setZoomFactor(currentZoomFactor);
+    // Prevent interaction with the parent window while the dialog is open.
+    saveFileDialogPointer->setWindowModality(Qt::WindowModal);
+
+    // Process the saving of the file.  The save file dialog pointer must be captured directly instead of by reference or nasty crashes occur.
+    auto saveFile = [saveFileDialogPointer, downloadUrl, this] ()
+    {
+        // Get the save location.  The dialog box should only allow the selecting of one file location.
+        QUrl saveLocation = saveFileDialogPointer->selectedUrls().value(0);
+
+        // Update the download directory if specified.
+        if (Settings::autoUpateDownloadDirectory())
+            updateDownloadDirectory(saveLocation.toLocalFile());
+
+        // Create a file copy job.  `-1` creates the file with default permissions.
+        KIO::FileCopyJob *fileCopyJobPointer = KIO::file_copy(downloadUrl, saveLocation, -1, KIO::Overwrite);
+
+        // Set the download job to display any warning and error messages.
+        fileCopyJobPointer->uiDelegate()->setAutoWarningHandlingEnabled(true);
+        fileCopyJobPointer->uiDelegate()->setAutoErrorHandlingEnabled(true);
+
+        // Start the download.
+        fileCopyJobPointer->start();
+    };
+
+    // Handle clicks on the save button.
+    connect(saveFileDialogPointer, &QDialog::accepted, this, saveFile);
+
+    // Show the dialog.
+    saveFileDialogPointer->show();
 }