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