]> gitweb.stoutner.com Git - PrivacyBrowserPC.git/blobdiff - src/widgets/TabWidget.cpp
Enable downloading of files that require login cookies. https://redmine.stoutner...
[PrivacyBrowserPC.git] / src / widgets / TabWidget.cpp
index 9e843ef306da564f362d7c5e434a0589b7dda21d..4edb304d0db6c45ed5d757a6d4a5f6cd20ad5596 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2022 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2022 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser PC <https://www.stoutner.com/privacy-browser-pc>.
  *
 #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 <QPrinter>
 
 // Initialize the public static variables.
-QString TabWidget::webEngineDefaultUserAgent = QStringLiteral("");
+QString TabWidget::webEngineDefaultUserAgent = QLatin1String("");
 
 // Construct the class.
 TabWidget::TabWidget(QWidget *parent) : QWidget(parent)
 {
+    // Instantiate the user agent helper.
+    userAgentHelperPointer = new UserAgentHelper();
+
     // Instantiate the UIs.
     Ui::TabWidget tabWidgetUi;
     Ui::AddTabWidget addTabWidgetUi;
@@ -112,14 +115,14 @@ void TabWidget::addCookieToStore(QNetworkCookie cookie, QWebEngineCookieStore *w
     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.
@@ -141,7 +144,7 @@ void TabWidget::addFirstTab()
     tabWidgetPointer->currentWidget()->setFocus();
 }
 
-PrivacyWebEngineView* TabWidget::addTab(const bool focusNewWebEngineView)
+PrivacyWebEngineView* TabWidget::addTab(const bool removeUrlLineEditFocus, const bool backgroundTab)
 {
     // Create a privacy WebEngine view.
     PrivacyWebEngineView *privacyWebEngineViewPointer = new PrivacyWebEngineView();
@@ -153,7 +156,7 @@ PrivacyWebEngineView* TabWidget::addTab(const bool focusNewWebEngineView)
     tabWidgetPointer->setTabIcon(newTabIndex, defaultTabIcon);
 
     // Create an off-the-record profile (the default when no profile name is specified).
-    QWebEngineProfile *webEngineProfilePointer = new QWebEngineProfile(QStringLiteral(""));
+    QWebEngineProfile *webEngineProfilePointer = new QWebEngineProfile(QLatin1String(""));
 
     // Create a WebEngine page.
     QWebEnginePage *webEnginePagePointer = new QWebEnginePage(webEngineProfilePointer);
@@ -166,12 +169,58 @@ PrivacyWebEngineView* TabWidget::addTab(const bool focusNewWebEngineView)
     QWebEngineSettings *webEngineSettingsPointer = webEnginePagePointer->settings();
 
     // Update the URL line edit when the URL changes.
-    connect(privacyWebEngineViewPointer, SIGNAL(urlChanged(const QUrl)), this, SLOT(updateUrl(const QUrl)));
+    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::urlChanged, [privacyWebEngineViewPointer, this] (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());
+        }
+
+        // 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.
+        privacyWebEngineViewPointer->setZoomFactor(currentZoomFactor);
+    });
+
+    // Update the progress bar when a load is started.
+    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::loadStarted, [privacyWebEngineViewPointer, this] ()
+    {
+        // Store the load progress.
+        privacyWebEngineViewPointer->loadProgressInt = 0;
+
+        // Show the progress bar if this is the current tab.
+        if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
+            emit showProgressBar(0);
+    });
+
+    // Update the progress bar when a load progresses.
+    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::loadProgress, [privacyWebEngineViewPointer, this] (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, [privacyWebEngineViewPointer, this] ()
+    {
+        // Store the load progress.
+        privacyWebEngineViewPointer->loadProgressInt = -1;
+
+        // Hide the progress bar if this is the current tab.
+        if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
+            emit hideProgressBar();
+    });
+
+    // 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)));
@@ -236,16 +285,13 @@ PrivacyWebEngineView* TabWidget::addTab(const bool focusNewWebEngineView)
     // Limit WebRTC to public IP addresses.
     webEngineSettingsPointer->setAttribute(QWebEngineSettings::WebRTCPublicInterfacesOnly, true);
 
-    // Define an update cookie count lambda.
-    auto updateCookieCount = [privacyWebEngineViewPointer, this] (const int numberOfCookies)
+    // Update the cookies action.
+    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::updateCookiesAction, [privacyWebEngineViewPointer, this] (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,21 +304,22 @@ PrivacyWebEngineView* TabWidget::addTab(const bool focusNewWebEngineView)
     for (QNetworkCookie *cookiePointer : *durableCookiesListPointer)
         addCookieToStore(*cookiePointer, webEngineCookieStorePointer);
 
-    // Define an update tab title lambda.
-    auto updateTabTitle = [privacyWebEngineViewPointer, this] (const QString &title)
+    // Update the title when it changes.
+    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::titleChanged, [this, privacyWebEngineViewPointer] (const QString &title)
     {
         // Get the index for this tab.
         int tabIndex = tabWidgetPointer->indexOf(privacyWebEngineViewPointer);
 
         // Update the title for this tab.
         tabWidgetPointer->setTabText(tabIndex, title);
-    };
 
-    // Update the title when it changes.
-    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::titleChanged, this, updateTabTitle);
+        // Update the window title if this is the current tab.
+        if (tabIndex == tabWidgetPointer->currentIndex())
+            emit updateWindowTitle(title);
+    });
 
-    // Define an update tab icon lambda.
-    auto updateTabIcon = [privacyWebEngineViewPointer, this] (const QIcon &icon)
+    // Update the icon when it changes.
+    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::iconChanged, [privacyWebEngineViewPointer, this] (const QIcon &icon)
     {
         // Get the index for this tab.
         int tabIndex = tabWidgetPointer->indexOf(privacyWebEngineViewPointer);
@@ -282,16 +329,23 @@ PrivacyWebEngineView* TabWidget::addTab(const bool focusNewWebEngineView)
             tabWidgetPointer->setTabIcon(tabIndex, defaultTabIcon);
         else
             tabWidgetPointer->setTabIcon(tabIndex, icon);
-    };
+    });
 
-    // Update the icon when it changes.
-    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::iconChanged, this, updateTabIcon);
+    // Enable spell checking.
+    webEngineProfilePointer->setSpellCheckEnabled(true);
 
-    // Move to the new tab.
-    tabWidgetPointer->setCurrentIndex(newTabIndex);
+    // Set the spell check language.
+    webEngineProfilePointer->setSpellCheckLanguages({QLatin1String("en_US")});
+
+    // 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());
+
+    // Move to the new tab if it is not a background tab.
+    if (!backgroundTab)
+        tabWidgetPointer->setCurrentIndex(newTabIndex);
 
     // Clear the URL line edit focus so that it populates correctly when opening a new tab from the context menu.
-    if (focusNewWebEngineView)
+    if (removeUrlLineEditFocus)
         emit clearUrlLineEditFocus();
 
     // Return the privacy WebEngine view pointer.
@@ -403,7 +457,7 @@ void TabWidget::applyDomainSettings(const QString &hostname, const bool reloadWe
         // Set the DOM storage status.
         switch (domainRecord.field(DomainsDatabase::DOM_STORAGE).value().toInt())
         {
-            // Set the default DOM storage status.
+            // Set the default DOM storage status.  QWebEngineSettings confusingly calls this local storage.
             case (DomainsDatabase::SYSTEM_DEFAULT):
             {
                 currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, Settings::domStorageEnabled());
@@ -411,7 +465,7 @@ void TabWidget::applyDomainSettings(const QString &hostname, const bool reloadWe
                 break;
             }
 
-            // Disable DOM storage.
+            // Disable DOM storage.  QWebEngineSettings confusingly calls this local storage.
             case (DomainsDatabase::DISABLED):
             {
                 currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, false);
@@ -419,7 +473,7 @@ void TabWidget::applyDomainSettings(const QString &hostname, const bool reloadWe
                 break;
             }
 
-            // Enable DOM storage.
+            // Enable DOM storage.  QWebEngineSettings confusingly calls this local storage.
             case (DomainsDatabase::ENABLED):
             {
                 currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, true);
@@ -449,7 +503,7 @@ void TabWidget::applyDomainSettings(const QString &hostname, const bool reloadWe
     else  // The hostname does not have domain settings.
     {
         // Reset the domain settings name.
-        currentPrivacyWebEngineViewPointer->domainSettingsName = QStringLiteral("");
+        currentPrivacyWebEngineViewPointer->domainSettingsName = QLatin1String("");
 
         // Set the JavaScript status.
         currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, Settings::javaScriptEnabled());
@@ -471,7 +525,7 @@ void TabWidget::applyDomainSettings(const QString &hostname, const bool reloadWe
     }
 
     // Update the UI.
-    emit updateDomainSettingsIndicator(currentPrivacyWebEngineViewPointer->domainSettingsName != QStringLiteral(""));
+    emit updateDomainSettingsIndicator(currentPrivacyWebEngineViewPointer->domainSettingsName != QLatin1String(""));
     emit updateJavaScriptAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::JavascriptEnabled));
     emit updateLocalStorageAction(currentPrivacyWebEngineViewPointer->localStorageEnabled);
     emit updateDomStorageAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::LocalStorageEnabled));
@@ -507,7 +561,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);
@@ -568,6 +622,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 temporarially 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.
@@ -601,10 +701,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()
@@ -628,18 +731,6 @@ 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.
@@ -765,59 +856,159 @@ void TabWidget::setTabBarVisible(const bool visible) const
     tabWidgetPointer->tabBar()->setVisible(visible);
 }
 
-void TabWidget::showSaveDialog(QWebEngineDownloadItem *downloadItemPointer) const
+void TabWidget::showSaveDialog(QWebEngineDownloadItem *webEngineDownloadItemPointer)
 {
-    // Instantiate the save dialog.
-    SaveDialog *saveDialogPointer = new SaveDialog(downloadItemPointer);
+    // Get the download attributes.
+    QUrl downloadUrl = webEngineDownloadItemPointer->url();
+    QString mimeTypeString = webEngineDownloadItemPointer->mimeType();
+    QString suggestedFileName = webEngineDownloadItemPointer->suggestedFileName();
+    int totalBytes = webEngineDownloadItemPointer->totalBytes();
+
+    // Check to see if local storage (cookies) is enabled.
+    if (currentPrivacyWebEngineViewPointer->localStorageEnabled)  // Local storage (cookies) is enabled.  Use WebEngine's downloader.
+    {
+        // Instantiate the save dialog.
+        SaveDialog *saveDialogPointer = new SaveDialog(downloadUrl, mimeTypeString, totalBytes);
 
-    // Connect the save button.
-    connect(saveDialogPointer, SIGNAL(showSaveFilePickerDialog(QUrl &, QString &)), this, SLOT(showSaveFilePickerDialog(QUrl &, QString &)));
+        // Display the save dialog.
+        int saveDialogResult = saveDialogPointer->exec();
 
-    // Show the dialog.
-    saveDialogPointer->show();
-}
+        // Process the save dialog results.
+        if (saveDialogResult == QDialog::Accepted)  // Save was selected.
+        {
+            // Get the download directory.
+            QString downloadDirectory = Settings::downloadLocation();
 
-void TabWidget::showSaveFilePickerDialog(QUrl &downloadUrl, QString &suggestedFileName)
-{
-    // Get the download location.
-    QString downloadDirectory = Settings::downloadLocation();
+            // Resolve the system download directory if specified.
+            if (downloadDirectory == QLatin1String("System Download Directory"))
+                downloadDirectory = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
 
-    // Resolve the system download directory if specified.
-    if (downloadDirectory == QStringLiteral("System Download Directory"))
-        downloadDirectory = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
+            // Display a save file dialog.
+            QString saveFilePath = QFileDialog::getSaveFileName(this, i18nc("Save file dialog caption", "Save File"), downloadDirectory + QLatin1Char('/') + suggestedFileName);
 
-    // Create a save file dialog.
-    QFileDialog *saveFileDialogPointer = new QFileDialog(this, i18nc("Save file dialog caption", "Save File"), downloadDirectory);
+            // Process the save file path.
+            if (!saveFilePath.isEmpty())  // The file save path is populated.
+            {
+                // Create a save file path file info.
+                QFileInfo saveFilePathFileInfo = QFileInfo(saveFilePath);
 
-    // Tell the dialog to use a save button.
-    saveFileDialogPointer->setAcceptMode(QFileDialog::AcceptSave);
+                // Get the canonical save path and file name.
+                QString absoluteSavePath = saveFilePathFileInfo.absolutePath();
+                QString saveFileName = saveFilePathFileInfo.fileName();
 
-    // Populate the file name from the download item pointer.
-    saveFileDialogPointer->selectFile(suggestedFileName);
+                // Set the download directory and file name.
+                webEngineDownloadItemPointer->setDownloadDirectory(absoluteSavePath);
+                webEngineDownloadItemPointer->setDownloadFileName(saveFileName);
 
-    // Prevent interaction with the parent window while the dialog is open.
-    saveFileDialogPointer->setWindowModality(Qt::WindowModal);
+                // Create a file download notification.
+                KNotification *fileDownloadNotificationPointer = new KNotification(QLatin1String("FileDownload"));
 
-    // 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);
+                // Set the notification title.
+                fileDownloadNotificationPointer->setTitle(i18nc("Download notification title", "Download"));
 
-        // 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 notification text.
+                fileDownloadNotificationPointer->setText(i18nc("Downloading notification text", "Downloading %1", saveFileName));
 
-        // Set the download job to display any error messages.
-        fileCopyJobPointer->uiDelegate()->setAutoErrorHandlingEnabled(true);
+                // Set the notification icon.
+                fileDownloadNotificationPointer->setIconName(QLatin1String("download"));
 
-        // Start the download.
-        fileCopyJobPointer->start();
-    };
+                // Set the action list cancel button.
+                fileDownloadNotificationPointer->setActions(QStringList({i18nc("Download notification action","Cancel")}));
 
-    // Handle clicks on the save button.
-    connect(saveFileDialogPointer, &QDialog::accepted, this, saveFile);
+                // Set the notification to display indefinitely.
+                fileDownloadNotificationPointer->setFlags(KNotification::Persistent);
 
-    // Show the dialog.
-    saveFileDialogPointer->show();
+                // Prevent the notification from being autodeleted if it is closed.  Otherwise, the updates to the notification below cause a crash.
+                fileDownloadNotificationPointer->setAutoDelete(false);
+
+                // Display the notification.
+                fileDownloadNotificationPointer->sendEvent();
+
+                // 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)
+                {
+                    // Calculate the download percentage.
+                    int downloadPercentage = 100 * bytesReceived / totalBytes;
+
+                    // Set the new text.  Total bytes will be 0 if the download size is unknown.
+                    if (totalBytes > 0)
+                        fileDownloadNotificationPointer->setText(i18nc("Download progress notification text", "%1\% of %2 downloaded (%3 of %4 bytes)", downloadPercentage, saveFileName,
+                                                                    bytesReceived, totalBytes));
+                    else
+                        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();
+                });
+
+                // 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  // 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(downloadUrl, mimeTypeString, totalBytes, suggestedFileName, true);
+
+        // Connect the save button.
+        connect(saveDialogPointer, SIGNAL(useNativeDownloader(QUrl &, QString &)), this, SLOT(useNativeDownloader(QUrl &, QString &)));
+
+        // Show the dialog.
+        saveDialogPointer->show();
+    }
 }
 
 void TabWidget::toggleDomStorage() const
@@ -832,6 +1023,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.
@@ -869,26 +1075,72 @@ void TabWidget::updateUiWithTabSettings()
     // Clear the URL line edit focus.
     emit clearUrlLineEditFocus();
 
-    // Update the UI.
-    emit updateDomainSettingsIndicator(currentPrivacyWebEngineViewPointer->domainSettingsName != QStringLiteral(""));
+    // Update the actions.
+    emit updateBackAction(currentWebEngineHistoryPointer->canGoBack());
+    emit updateCookiesAction(currentPrivacyWebEngineViewPointer->cookieListPointer->size());
+    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());
+
+    // 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::useNativeDownloader(QUrl &downloadUrl, QString &suggestedFileName)
 {
-    // Update the URL line edit.
-    emit updateUrlLineEdit(url);
+    // Get the download directory.
+    QString downloadDirectory = Settings::downloadLocation();
 
-    // 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);
 
-    // 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);
+    // Tell the dialog to use a save button.
+    saveFileDialogPointer->setAcceptMode(QFileDialog::AcceptSave);
+
+    // Populate the file name from the download item pointer.
+    saveFileDialogPointer->selectFile(suggestedFileName);
+
+    // 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] ()
+    {
+        // Get the save location.  The dialog box should only allow the selecting of one file location.
+        QUrl saveLocation = saveFileDialogPointer->selectedUrls().value(0);
+
+        // 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 error messages.
+        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();
 }