]> 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 eb82f23d2fed09c49c4739479963ff2aba519fc5..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);
@@ -216,6 +219,9 @@ PrivacyWebEngineView* TabWidget::addTab(const bool focusNewWebEngineView)
             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)));
 
@@ -325,11 +331,21 @@ PrivacyWebEngineView* TabWidget::addTab(const bool focusNewWebEngineView)
             tabWidgetPointer->setTabIcon(tabIndex, icon);
     });
 
-    // Move to the new tab.
-    tabWidgetPointer->setCurrentIndex(newTabIndex);
+    // Enable spell checking.
+    webEngineProfilePointer->setSpellCheckEnabled(true);
+
+    // 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.
@@ -441,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());
@@ -449,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);
@@ -457,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);
@@ -487,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());
@@ -509,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));
@@ -545,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);
@@ -606,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.
@@ -794,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
@@ -861,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.
@@ -898,22 +1075,72 @@ void TabWidget::updateUiWithTabSettings()
     // Clear the URL line edit focus.
     emit clearUrlLineEditFocus();
 
-    // Update the UI.
+    // Update the actions.
     emit updateBackAction(currentWebEngineHistoryPointer->canGoBack());
     emit updateCookiesAction(currentPrivacyWebEngineViewPointer->cookieListPointer->size());
-    emit updateDomainSettingsIndicator(currentPrivacyWebEngineViewPointer->domainSettingsName != QStringLiteral(""));
     emit updateDomStorageAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::LocalStorageEnabled));
     emit updateForwardAction(currentWebEngineHistoryPointer->canGoForward());
     emit updateJavaScriptAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::JavascriptEnabled));
     emit updateLocalStorageAction(currentPrivacyWebEngineViewPointer->localStorageEnabled);
-    emit updateWindowTitle(currentPrivacyWebEngineViewPointer->title());
-    emit updateUrlLineEdit(currentPrivacyWebEngineViewPointer->url());
     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());
+
+    // 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::useNativeDownloader(QUrl &downloadUrl, QString &suggestedFileName)
+{
+    // Get the download directory.
+    QString downloadDirectory = Settings::downloadLocation();
+
+    // 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);
+
+    // 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();
+}