Simple Browser

Simple Browser demonstrates how to use the Qt WebEngine Widgets classes to develop a small Web browser application that contains the following elements:

  • Menu bar for opening stored pages and managing windows and tabs.

  • Navigation bar for entering a URL and for moving backward and forward in the web page browsing history.

  • Multi-tab area for displaying web content within tabs.

  • Status bar for displaying hovered links.

  • A simple download manager.

The web content can be opened in new tabs or separate windows. HTTP and proxy authentication can be used for accessing web pages.

Class Hierarchy

We will implement the following main classes:

  • Browser is a class managing the application windows.

  • BrowserWindow is a QMainWindow showing the menu, a navigation

    bar, TabWidget, and a status bar.

  • TabWidget is a QTabWidget and contains one or multiple

    browser tabs.

  • WebView is a QWebEngineView, provides a view for WebPage,

    and is added as a tab in TabWidget.

  • WebPage is a QWebEnginePage that represents website content.

Additionally, we will implement some auxiliary classes:

  • WebPopupWindow is a QWidget for showing popup windows.

  • DownloadManagerWidget is a QWidget implementing the downloads

    list.

Creating the Browser Main Window

This example supports multiple main windows that are owned by a Browser object. This class also owns the DownloadManagerWidget and could be used for further functionality, such as bookmarks and history managers.

In main.cpp, we create the first BrowserWindow instance and add it to the Browser object. If no arguments are passed on the command line, we open the Qt Homepage.

To suppress flicker when switching the window to OpenGL rendering, we call show after the first browser tab has been added.

Creating Tabs

The BrowserWindow constructor initializes all the necessary user interface related objects. The centralWidget of BrowserWindow contains an instance of TabWidget. The TabWidget contains one or several WebView instances as tabs, and delegates it’s signals and slots to the currently selected one.

In TabWidget.setup_view(), we make sure that the TabWidget always forwards the signals of the currently selected WebView.

Implementing WebView Functionality

The class WebView is derived from QWebEngineView to support the following functionality:

  • Displaying error messages in case the render process dies

  • Handling createWindow() requests

  • Adding custom menu items to context menus

Managing WebWindows

The loaded page might want to create windows of the type QWebEnginePage.WebWindowType, for example, when a JavaScript program requests to open a document in a new window or dialog. This is handled by overriding QWebView.createWindow().

In case of QWebEnginePage.WebDialog, we create an instance of a custom WebPopupWindow class.

Adding Context Menu Items

We add a menu item to the context menu, so that users can right-click to have an inspector opened in a new window. We override QWebEngineView.contextMenuEvent() and use QWebEnginePage.createStandardContextMenu() to create a default QMenu with a default list of QWebEnginePage.WebAction actions.

Implementing WebPage and WebView Functionality

We implement WebPage as a subclass of QWebEnginePage and WebView as as subclass of QWebEngineView to enable HTTP, proxy authentication, as well as ignoring SSL certificate errors when accessing web pages.

In all the cases above, we display the appropriate dialog to the user. In case of authentication, we need to set the correct credential values on the QAuthenticator object.

The handleProxyAuthenticationRequired signal handler implements the very same steps for the authentication of HTTP proxies.

In case of SSL errors, we just need to return a boolean value indicating whether the certificate should be ignored.

Opening a Web Page

This section describes the workflow for opening a new page. When the user enters a URL in the navigation bar and presses Enter, the QLineEdit.:returnPressed() signal is emitted and the new URL is then handed over to TabWidget.set_url().

The call is forwarded to the currently selected tab.

The set_url() method of WebView just forwards the url to the associated WebPage, which in turn starts the downloading of the page’s content in the background.

Implementing Private Browsing

Private browsing, incognito mode, or off-the-record mode is a feature of many browsers where normally persistent data, such as cookies, the HTTP cache, or browsing history, is kept only in memory, leaving no trace on disk. In this example we will implement private browsing on the window level with tabs in one window all in either normal or private mode. Alternatively we could implement private browsing on the tab-level, with some tabs in a window in normal mode, others in private mode.

Implementing private browsing is quite easy using Qt WebEngine. All one has to do is to create a new QWebEngineProfile and use it in the QWebEnginePage instead of the default profile. In the example, this new profile is owned by the Browser object.

The required profile for private browsing is created together with its first window. The default constructor for QWebEngineProfile already puts it in off-the-record mode.

All that is left to do is to pass the appropriate profile down to the appropriate QWebEnginePage objects. The Browser object will hand to each new BrowserWindow either the global default profile or one shared off-the-record profile instance.

The BrowserWindow and TabWidget objects will then ensure that all QWebEnginePage objects contained in a window will use this profile.

Managing Downloads

Downloads are associated with a QWebEngineProfile. Whenever a download is triggered on a web page the QWebEngineProfile.downloadRequested signal is emitted with a QWebEngineDownloadRequest, which in this example is forwarded to DownloadManagerWidget.download_requested().

This method prompts the user for a file name (with a pre-filled suggestion) and starts the download (unless the user cancels the Save As dialog).

The QWebEngineDownloadRequest object will periodically emit the QWebEngineDownloadRequest.receivedBytesChanged() signal to notify potential observers of the download progress and the QWebEngineDownloadRequest.stateChanged() signal when the download is finished or when an error occurs.

Files and Attributions

The example uses icons from the Tango Icon Library.

Simple Browser Screenshot

Download this example

# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

"""PySide6 port of the Qt WebEngineWidgets Simple Browser example from Qt v6.x"""

import sys
from argparse import ArgumentParser, RawTextHelpFormatter

from PySide6.QtWebEngineCore import QWebEngineProfile, QWebEngineSettings
from PySide6.QtWidgets import QApplication
from PySide6.QtGui import QIcon
from PySide6.QtCore import QCoreApplication, QLoggingCategory, QUrl

from browser import Browser

import data.rc_simplebrowser  # noqa: F401

if __name__ == "__main__":
    parser = ArgumentParser(description="Qt Widgets Web Browser",
                            formatter_class=RawTextHelpFormatter)
    parser.add_argument("--single-process", "-s", action="store_true",
                        help="Run in single process mode (trouble shooting)")
    parser.add_argument("url", type=str, nargs="?", help="URL")
    args = parser.parse_args()

    QCoreApplication.setOrganizationName("QtExamples")

    app_args = sys.argv
    if args.single_process:
        app_args.extend(["--webEngineArgs", "--single-process"])
    app = QApplication(app_args)
    app.setWindowIcon(QIcon(":AppLogoColor.png"))
    QLoggingCategory.setFilterRules("qt.webenginecontext.debug=true")

    s = QWebEngineProfile.defaultProfile().settings()
    s.setAttribute(QWebEngineSettings.PluginsEnabled, True)
    s.setAttribute(QWebEngineSettings.DnsPrefetchEnabled, True)

    browser = Browser()
    window = browser.create_hidden_window()

    url = QUrl.fromUserInput(args.url) if args.url else QUrl("https://www.qt.io")
    window.tab_widget().set_url(url)
    window.show()
    sys.exit(app.exec())
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtWebEngineCore import (qWebEngineChromiumVersion,
                                     QWebEngineProfile, QWebEngineSettings)
from PySide6.QtCore import QObject, Qt, Slot

from downloadmanagerwidget import DownloadManagerWidget
from browserwindow import BrowserWindow


class Browser(QObject):

    def __init__(self, parent=None):
        super().__init__(parent)
        self._windows = []
        self._download_manager_widget = DownloadManagerWidget()
        self._profile = None

        # Quit application if the download manager window is the only
        # remaining window
        self._download_manager_widget.setAttribute(Qt.WA_QuitOnClose, False)

        dp = QWebEngineProfile.defaultProfile()
        dp.downloadRequested.connect(self._download_manager_widget.download_requested)

    def create_hidden_window(self, offTheRecord=False):
        if not offTheRecord and not self._profile:
            name = "simplebrowser." + qWebEngineChromiumVersion()
            self._profile = QWebEngineProfile(name)
            s = self._profile.settings()
            s.setAttribute(QWebEngineSettings.PluginsEnabled, True)
            s.setAttribute(QWebEngineSettings.DnsPrefetchEnabled, True)
            s.setAttribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls, True)
            s.setAttribute(QWebEngineSettings.LocalContentCanAccessFileUrls, False)
            self._profile.downloadRequested.connect(
                self._download_manager_widget.download_requested)

        profile = QWebEngineProfile.defaultProfile() if offTheRecord else self._profile
        main_window = BrowserWindow(self, profile, False)
        self._windows.append(main_window)
        main_window.about_to_close.connect(self._remove_window)
        return main_window

    def create_window(self, offTheRecord=False):
        main_window = self.create_hidden_window(offTheRecord)
        main_window.show()
        return main_window

    def create_dev_tools_window(self):
        profile = (self._profile if self._profile
                   else QWebEngineProfile.defaultProfile())
        main_window = BrowserWindow(self, profile, True)
        self._windows.append(main_window)
        main_window.about_to_close.connect(self._remove_window)
        main_window.show()
        return main_window

    def windows(self):
        return self._windows

    def download_manager_widget(self):
        return self._download_manager_widget

    @Slot()
    def _remove_window(self):
        w = self.sender()
        if w in self._windows:
            del self._windows[self._windows.index(w)]
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

import sys

from PySide6.QtWebEngineCore import QWebEnginePage
from PySide6.QtWidgets import (QMainWindow, QFileDialog,
                               QInputDialog, QLineEdit, QMenu, QMessageBox,
                               QProgressBar, QToolBar, QVBoxLayout, QWidget)
from PySide6.QtGui import QAction, QGuiApplication, QIcon, QKeySequence
from PySide6.QtCore import QUrl, Qt, Slot, Signal

from tabwidget import TabWidget


def remove_backspace(keys):
    result = keys.copy()
    # Chromium already handles navigate on backspace when appropriate.
    for i, key in enumerate(result):
        if (key[0].key() & Qt.Key_unknown) == Qt.Key_Backspace:
            del result[i]
            break
    return result


class BrowserWindow(QMainWindow):

    about_to_close = Signal()

    def __init__(self, browser, profile, forDevTools):
        super().__init__()

        self._progress_bar = None
        self._history_back_action = None
        self._history_forward_action = None
        self._stop_action = None
        self._reload_action = None
        self._stop_reload_action = None
        self._url_line_edit = None
        self._fav_action = None
        self._last_search = ""
        self._toolbar = None

        self._browser = browser
        self._profile = profile
        self._tab_widget = TabWidget(profile, self)

        self._stop_icon = QIcon.fromTheme(QIcon.ThemeIcon.ProcessStop,
                                          QIcon(":process-stop.png"))
        self._reload_icon = QIcon.fromTheme(QIcon.ThemeIcon.ViewRefresh,
                                            QIcon(":view-refresh.png"))

        self.setAttribute(Qt.WA_DeleteOnClose, True)
        self.setFocusPolicy(Qt.ClickFocus)

        if not forDevTools:
            self._progress_bar = QProgressBar(self)

            self._toolbar = self.create_tool_bar()
            self.addToolBar(self._toolbar)
            mb = self.menuBar()
            mb.addMenu(self.create_file_menu(self._tab_widget))
            mb.addMenu(self.create_edit_menu())
            mb.addMenu(self.create_view_menu())
            mb.addMenu(self.create_window_menu(self._tab_widget))
            mb.addMenu(self.create_help_menu())

        central_widget = QWidget(self)
        layout = QVBoxLayout(central_widget)
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        if not forDevTools:
            self.addToolBarBreak()

            self._progress_bar.setMaximumHeight(1)
            self._progress_bar.setTextVisible(False)
            s = "QProgressBar {border: 0px} QProgressBar.chunk {background-color: #da4453}"
            self._progress_bar.setStyleSheet(s)

            layout.addWidget(self._progress_bar)

        layout.addWidget(self._tab_widget)
        self.setCentralWidget(central_widget)

        self._tab_widget.title_changed.connect(self.handle_web_view_title_changed)
        if not forDevTools:
            self._tab_widget.link_hovered.connect(self._show_status_message)
            self._tab_widget.load_progress.connect(self.handle_web_view_load_progress)
            self._tab_widget.web_action_enabled_changed.connect(
                self.handle_web_action_enabled_changed)
            self._tab_widget.url_changed.connect(self._url_changed)
            self._tab_widget.fav_icon_changed.connect(self._fav_action.setIcon)
            self._tab_widget.dev_tools_requested.connect(self.handle_dev_tools_requested)
            self._url_line_edit.returnPressed.connect(self._address_return_pressed)
            self._tab_widget.find_text_finished.connect(self.handle_find_text_finished)

            focus_url_line_edit_action = QAction(self)
            self.addAction(focus_url_line_edit_action)
            focus_url_line_edit_action.setShortcut(QKeySequence(Qt.CTRL | Qt.Key_L))
            focus_url_line_edit_action.triggered.connect(self._focus_url_lineEdit)

        self.handle_web_view_title_changed("")
        self._tab_widget.create_tab()

    @Slot(str)
    def _show_status_message(self, m):
        self.statusBar().showMessage(m)

    @Slot(QUrl)
    def _url_changed(self, url):
        self._url_line_edit.setText(url.toDisplayString())

    @Slot()
    def _address_return_pressed(self):
        url = QUrl.fromUserInput(self._url_line_edit.text())
        self._tab_widget.set_url(url)

    @Slot()
    def _focus_url_lineEdit(self):
        self._url_line_edit.setFocus(Qt.ShortcutFocusReason)

    @Slot()
    def _new_tab(self):
        self._tab_widget.create_tab()
        self._url_line_edit.setFocus()

    @Slot()
    def _close_current_tab(self):
        self._tab_widget.close_tab(self._tab_widget.currentIndex())

    @Slot()
    def _update_close_action_text(self):
        last_win = len(self._browser.windows()) == 1
        self._close_action.setText("Quit" if last_win else "Close Window")

    def sizeHint(self):
        desktop_rect = QGuiApplication.primaryScreen().geometry()
        return desktop_rect.size() * 0.9

    def create_file_menu(self, tabWidget):
        file_menu = QMenu("File")
        file_menu.addAction("&New Window", QKeySequence.New,
                            self.handle_new_window_triggered)
        file_menu.addAction("New &Incognito Window",
                            self.handle_new_incognito_window_triggered)

        new_tab_action = QAction("New Tab", self)
        new_tab_action.setShortcuts(QKeySequence.AddTab)
        new_tab_action.triggered.connect(self._new_tab)
        file_menu.addAction(new_tab_action)

        file_menu.addAction("&Open File...", QKeySequence.Open,
                            self.handle_file_open_triggered)
        file_menu.addSeparator()

        close_tab_action = QAction("Close Tab", self)
        close_tab_action.setShortcuts(QKeySequence.Close)
        close_tab_action.triggered.connect(self._close_current_tab)
        file_menu.addAction(close_tab_action)

        self._close_action = QAction("Quit", self)
        self._close_action.setShortcut(QKeySequence(Qt.CTRL | Qt.Key_Q))
        self._close_action.triggered.connect(self.close)
        file_menu.addAction(self._close_action)

        file_menu.aboutToShow.connect(self._update_close_action_text)
        return file_menu

    @Slot()
    def _find_next(self):
        tab = self.current_tab()
        if tab and self._last_search:
            tab.findText(self._last_search)

    @Slot()
    def _find_previous(self):
        tab = self.current_tab()
        if tab and self._last_search:
            tab.findText(self._last_search, QWebEnginePage.FindBackward)

    def create_edit_menu(self):
        edit_menu = QMenu("Edit")
        find_action = edit_menu.addAction("Find")
        find_action.setShortcuts(QKeySequence.Find)
        find_action.triggered.connect(self.handle_find_action_triggered)

        find_next_action = edit_menu.addAction("Find Next")
        find_next_action.setShortcut(QKeySequence.FindNext)
        find_next_action.triggered.connect(self._find_next)

        find_previous_action = edit_menu.addAction("Find Previous")
        find_previous_action.setShortcut(QKeySequence.FindPrevious)
        find_previous_action.triggered.connect(self._find_previous)
        return edit_menu

    @Slot()
    def _stop(self):
        self._tab_widget.trigger_web_page_action(QWebEnginePage.Stop)

    @Slot()
    def _reload(self):
        self._tab_widget.trigger_web_page_action(QWebEnginePage.Reload)

    @Slot()
    def _zoom_in(self):
        tab = self.current_tab()
        if tab:
            tab.setZoomFactor(tab.zoomFactor() + 0.1)

    @Slot()
    def _zoom_out(self):
        tab = self.current_tab()
        if tab:
            tab.setZoomFactor(tab.zoomFactor() - 0.1)

    @Slot()
    def _reset_zoom(self):
        tab = self.current_tab()
        if tab:
            tab.setZoomFactor(1)

    @Slot()
    def _toggle_toolbar(self):
        if self._toolbar.isVisible():
            self._view_toolbar_action.setText("Show Toolbar")
            self._toolbar.close()
        else:
            self._view_toolbar_action.setText("Hide Toolbar")
            self._toolbar.show()

    @Slot()
    def _toggle_statusbar(self):
        sb = self.statusBar()
        if sb.isVisible():
            self._view_statusbar_action.setText("Show Status Bar")
            sb.close()
        else:
            self._view_statusbar_action.setText("Hide Status Bar")
            sb.show()

    def create_view_menu(self):
        view_menu = QMenu("View")
        self._stop_action = view_menu.addAction("Stop")
        shortcuts = []
        shortcuts.append(QKeySequence(Qt.CTRL | Qt.Key_Period))
        shortcuts.append(QKeySequence(Qt.Key_Escape))
        self._stop_action.setShortcuts(shortcuts)
        self._stop_action.triggered.connect(self._stop)

        self._reload_action = view_menu.addAction("Reload Page")
        self._reload_action.setShortcuts(QKeySequence.Refresh)
        self._reload_action.triggered.connect(self._reload)

        zoom_in = view_menu.addAction("Zoom In")
        zoom_in.setShortcut(QKeySequence(Qt.CTRL | Qt.Key_Plus))
        zoom_in.triggered.connect(self._zoom_in)

        zoom_out = view_menu.addAction("Zoom Out")
        zoom_out.setShortcut(QKeySequence(Qt.CTRL | Qt.Key_Minus))
        zoom_out.triggered.connect(self._zoom_out)

        reset_zoom = view_menu.addAction("Reset Zoom")
        reset_zoom.setShortcut(QKeySequence(Qt.CTRL | Qt.Key_0))
        reset_zoom.triggered.connect(self._reset_zoom)

        view_menu.addSeparator()
        self._view_toolbar_action = QAction("Hide Toolbar", self)
        self._view_toolbar_action.setShortcut("Ctrl+|")
        self._view_toolbar_action.triggered.connect(self._toggle_toolbar)
        view_menu.addAction(self._view_toolbar_action)

        self._view_statusbar_action = QAction("Hide Status Bar", self)
        self._view_statusbar_action.setShortcut("Ctrl+/")
        self._view_statusbar_action.triggered.connect(self._toggle_statusbar)
        view_menu.addAction(self._view_statusbar_action)
        return view_menu

    @Slot()
    def _emit_dev_tools_requested(self):
        tab = self.current_tab()
        if tab:
            tab.dev_tools_requested.emit(tab.page())

    def create_window_menu(self, tabWidget):
        menu = QMenu("Window")
        self._next_tab_action = QAction("Show Next Tab", self)
        shortcuts = []
        shortcuts.append(QKeySequence(Qt.CTRL | Qt.Key_BraceRight))
        shortcuts.append(QKeySequence(Qt.CTRL | Qt.Key_PageDown))
        shortcuts.append(QKeySequence(Qt.CTRL | Qt.Key_BracketRight))
        shortcuts.append(QKeySequence(Qt.CTRL | Qt.Key_Less))
        self._next_tab_action.setShortcuts(shortcuts)
        self._next_tab_action.triggered.connect(tabWidget.next_tab)

        self._previous_tab_action = QAction("Show Previous Tab", self)
        shortcuts.clear()
        shortcuts.append(QKeySequence(Qt.CTRL | Qt.Key_BraceLeft))
        shortcuts.append(QKeySequence(Qt.CTRL | Qt.Key_PageUp))
        shortcuts.append(QKeySequence(Qt.CTRL | Qt.Key_BracketLeft))
        shortcuts.append(QKeySequence(Qt.CTRL | Qt.Key_Greater))
        self._previous_tab_action.setShortcuts(shortcuts)
        self._previous_tab_action.triggered.connect(tabWidget.previous_tab)

        self._inspector_action = QAction("Open inspector in window", self)
        shortcuts.clear()
        shortcuts.append(QKeySequence(Qt.CTRL | Qt.SHIFT | Qt.Key_I))
        self._inspector_action.setShortcuts(shortcuts)
        self._inspector_action.triggered.connect(self._emit_dev_tools_requested)
        self._window_menu = menu
        menu.aboutToShow.connect(self._populate_window_menu)
        return menu

    def _populate_window_menu(self):
        menu = self._window_menu
        menu.clear()
        menu.addAction(self._next_tab_action)
        menu.addAction(self._previous_tab_action)
        menu.addSeparator()
        menu.addAction(self._inspector_action)
        menu.addSeparator()
        windows = self._browser.windows()
        index = 0
        title = self.window().windowTitle()
        for window in windows:
            action = menu.addAction(title, self.handle_show_window_triggered)
            action.setData(index)
            action.setCheckable(True)
            if window == self:
                action.setChecked(True)
            index += 1

    def create_help_menu(self):
        help_menu = QMenu("Help")
        help_menu.addAction("About Qt", qApp.aboutQt)  # noqa: F821
        return help_menu

    @Slot()
    def _back(self):
        self._tab_widget.trigger_web_page_action(QWebEnginePage.Back)

    @Slot()
    def _forward(self):
        self._tab_widget.trigger_web_page_action(QWebEnginePage.Forward)

    @Slot()
    def _stop_reload(self):
        a = self._stop_reload_action.data()
        self._tab_widget.trigger_web_page_action(QWebEnginePage.WebAction(a))

    def create_tool_bar(self):
        navigation_bar = QToolBar("Navigation")
        navigation_bar.setMovable(False)
        navigation_bar.toggleViewAction().setEnabled(False)

        self._history_back_action = QAction(self)
        back_shortcuts = remove_backspace(QKeySequence.keyBindings(QKeySequence.Back))

        # For some reason Qt doesn't bind the dedicated Back key to Back.
        back_shortcuts.append(QKeySequence(Qt.Key_Back))
        self._history_back_action.setShortcuts(back_shortcuts)
        self._history_back_action.setIconVisibleInMenu(False)
        back_icon = QIcon.fromTheme(QIcon.ThemeIcon.GoPrevious,
                                    QIcon(":go-previous.png"))
        self._history_back_action.setIcon(back_icon)
        self._history_back_action.setToolTip("Go back in history")
        self._history_back_action.triggered.connect(self._back)
        navigation_bar.addAction(self._history_back_action)

        self._history_forward_action = QAction(self)
        fwd_shortcuts = remove_backspace(QKeySequence.keyBindings(QKeySequence.Forward))
        fwd_shortcuts.append(QKeySequence(Qt.Key_Forward))
        self._history_forward_action.setShortcuts(fwd_shortcuts)
        self._history_forward_action.setIconVisibleInMenu(False)
        next_icon = QIcon.fromTheme(QIcon.ThemeIcon.GoNext,
                                    QIcon(":go-next.png"))
        self._history_forward_action.setIcon(next_icon)
        self._history_forward_action.setToolTip("Go forward in history")
        self._history_forward_action.triggered.connect(self._forward)
        navigation_bar.addAction(self._history_forward_action)

        self._stop_reload_action = QAction(self)
        self._stop_reload_action.triggered.connect(self._stop_reload)
        navigation_bar.addAction(self._stop_reload_action)

        self._url_line_edit = QLineEdit(self)
        self._fav_action = QAction(self)
        self._url_line_edit.addAction(self._fav_action, QLineEdit.LeadingPosition)
        self._url_line_edit.setClearButtonEnabled(True)
        navigation_bar.addWidget(self._url_line_edit)

        downloads_action = QAction(self)
        downloads_action.setIcon(QIcon(":go-bottom.png"))
        downloads_action.setToolTip("Show downloads")
        navigation_bar.addAction(downloads_action)
        dw = self._browser.download_manager_widget()
        downloads_action.triggered.connect(dw.show)

        return navigation_bar

    def handle_web_action_enabled_changed(self, action, enabled):
        if action == QWebEnginePage.Back:
            self._history_back_action.setEnabled(enabled)
        elif action == QWebEnginePage.Forward:
            self._history_forward_action.setEnabled(enabled)
        elif action == QWebEnginePage.Reload:
            self._reload_action.setEnabled(enabled)
        elif action == QWebEnginePage.Stop:
            self._stop_action.setEnabled(enabled)
        else:
            print("Unhandled webActionChanged signal", file=sys.stderr)

    def handle_web_view_title_changed(self, title):
        off_the_record = self._profile.isOffTheRecord()
        suffix = ("Qt Simple Browser (Incognito)" if off_the_record
                  else "Qt Simple Browser")
        if title:
            self.setWindowTitle(f"{title} - {suffix}")
        else:
            self.setWindowTitle(suffix)

    def handle_new_window_triggered(self):
        window = self._browser.create_window()
        window._url_line_edit.setFocus()

    def handle_new_incognito_window_triggered(self):
        window = self._browser.create_window(True)
        window._url_line_edit.setFocus()

    def handle_file_open_triggered(self):
        filter = "Web Resources (*.html *.htm *.svg *.png *.gif *.svgz);;All files (*.*)"
        url, _ = QFileDialog.getOpenFileUrl(self, "Open Web Resource", "", filter)
        if url:
            self.current_tab().setUrl(url)

    def handle_find_action_triggered(self):
        if not self.current_tab():
            return
        search, ok = QInputDialog.getText(self, "Find", "Find:",
                                          QLineEdit.Normal, self._last_search)
        if ok and search:
            self._last_search = search
            self.current_tab().findText(self._last_search)

    def closeEvent(self, event):
        count = self._tab_widget.count()
        if count > 1:
            m = f"Are you sure you want to close the window?\nThere are {count} tabs open."
            ret = QMessageBox.warning(self, "Confirm close", m,
                                      QMessageBox.Yes | QMessageBox.No,
                                      QMessageBox.No)
            if ret == QMessageBox.No:
                event.ignore()
                return

        event.accept()
        self.about_to_close.emit()
        self.deleteLater()

    def tab_widget(self):
        return self._tab_widget

    def current_tab(self):
        return self._tab_widget.current_web_view()

    def handle_web_view_load_progress(self, progress):
        if 0 < progress and progress < 100:
            self._stop_reload_action.setData(QWebEnginePage.Stop)
            self._stop_reload_action.setIcon(self._stop_icon)
            self._stop_reload_action.setToolTip("Stop loading the current page")
            self._progress_bar.setValue(progress)
        else:
            self._stop_reload_action.setData(QWebEnginePage.Reload)
            self._stop_reload_action.setIcon(self._reload_icon)
            self._stop_reload_action.setToolTip("Reload the current page")
            self._progress_bar.setValue(0)

    def handle_show_window_triggered(self):
        action = self.sender()
        if action:
            offset = action.data()
            window = self._browser.windows()[offset]
            window.activateWindow()
            window.current_tab().setFocus()

    def handle_dev_tools_requested(self, source):
        page = self._browser.create_dev_tools_window().current_tab().page()
        source.setDevToolsPage(page)
        source.triggerAction(QWebEnginePage.InspectElement)

    def handle_find_text_finished(self, result):
        sb = self.statusBar()
        if result.numberOfMatches() == 0:
            sb.showMessage(f'"{self._lastSearch}" not found.')
        else:
            active = result.activeMatch()
            number = result.numberOfMatches()
            sb.showMessage(f'"{self._last_search}" found: {active}/{number}')

    def browser(self):
        return self._browser
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>CertificateErrorDialog</class>
 <widget class="QDialog" name="CertificateErrorDialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>689</width>
    <height>204</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Dialog</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <property name="leftMargin">
    <number>20</number>
   </property>
   <property name="rightMargin">
    <number>20</number>
   </property>
   <item>
    <widget class="QLabel" name="m_iconLabel">
     <property name="text">
      <string>Icon</string>
     </property>
     <property name="alignment">
      <set>Qt::AlignmentFlag::AlignCenter</set>
     </property>
    </widget>
   </item>
   <item>
    <widget class="QLabel" name="m_errorLabel">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
       <horstretch>0</horstretch>
       <verstretch>0</verstretch>
      </sizepolicy>
     </property>
     <property name="text">
      <string>Error</string>
     </property>
     <property name="alignment">
      <set>Qt::AlignmentFlag::AlignCenter</set>
     </property>
     <property name="wordWrap">
      <bool>true</bool>
     </property>
    </widget>
   </item>
   <item>
    <widget class="QLabel" name="m_infoLabel">
     <property name="sizePolicy">
      <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
       <horstretch>0</horstretch>
       <verstretch>0</verstretch>
      </sizepolicy>
     </property>
     <property name="text">
      <string>If you wish so, you may continue with an unverified certificate. Accepting an unverified certificate mean you may not be connected with the host you tried to connect to.

Do you wish to override the security check and continue ?   </string>
     </property>
     <property name="alignment">
      <set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter</set>
     </property>
     <property name="wordWrap">
      <bool>true</bool>
     </property>
    </widget>
   </item>
   <item>
    <spacer name="verticalSpacer">
     <property name="orientation">
      <enum>Qt::Orientation::Vertical</enum>
     </property>
     <property name="sizeHint" stdset="0">
      <size>
       <width>20</width>
       <height>16</height>
      </size>
     </property>
    </spacer>
   </item>
   <item>
    <widget class="QDialogButtonBox" name="buttonBox">
     <property name="orientation">
      <enum>Qt::Orientation::Horizontal</enum>
     </property>
     <property name="standardButtons">
      <set>QDialogButtonBox::StandardButton::No|QDialogButtonBox::StandardButton::Yes</set>
     </property>
    </widget>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections>
  <connection>
   <sender>buttonBox</sender>
   <signal>accepted()</signal>
   <receiver>CertificateErrorDialog</receiver>
   <slot>accept()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>248</x>
     <y>254</y>
    </hint>
    <hint type="destinationlabel">
     <x>157</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
  <connection>
   <sender>buttonBox</sender>
   <signal>rejected()</signal>
   <receiver>CertificateErrorDialog</receiver>
   <slot>reject()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>316</x>
     <y>260</y>
    </hint>
    <hint type="destinationlabel">
     <x>286</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
 </connections>
</ui>
<RCC>
    <qresource prefix="/">
        <file>AppLogoColor.png</file>
        <file>ninja.png</file>
    </qresource>
    <qresource prefix="/">
        <file alias="dialog-error.png">3rdparty/dialog-error.png</file>
        <file alias="edit-clear.png">3rdparty/edit-clear.png</file>
        <file alias="go-bottom.png">3rdparty/go-bottom.png</file>
        <file alias="go-next.png">3rdparty/go-next.png</file>
        <file alias="go-previous.png">3rdparty/go-previous.png</file>
        <file alias="process-stop.png">3rdparty/process-stop.png</file>
        <file alias="text-html.png">3rdparty/text-html.png</file>
        <file alias="view-refresh.png">3rdparty/view-refresh.png</file>
    </qresource>
</RCC>
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtWebEngineCore import QWebEngineDownloadRequest
from PySide6.QtWidgets import QWidget, QFileDialog
from PySide6.QtCore import QDir, QFileInfo, Qt

from downloadwidget import DownloadWidget
from ui_downloadmanagerwidget import Ui_DownloadManagerWidget


# Displays a list of downloads.
class DownloadManagerWidget(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)
        self._ui = Ui_DownloadManagerWidget()
        self._num_downloads = 0
        self._ui.setupUi(self)

    def download_requested(self, download):
        assert (download and download.state() == QWebEngineDownloadRequest.DownloadRequested)

        proposal_dir = download.downloadDirectory()
        proposal_name = download.downloadFileName()
        proposal = QDir(proposal_dir).filePath(proposal_name)
        path, _ = QFileDialog.getSaveFileName(self, "Save as", proposal)
        if not path:
            return

        fi = QFileInfo(path)
        download.setDownloadDirectory(fi.path())
        download.setDownloadFileName(fi.fileName())
        download.accept()
        self.add(DownloadWidget(download))

        self.show()

    def add(self, downloadWidget):
        downloadWidget.remove_clicked.connect(self.remove)
        self._ui.m_itemsLayout.insertWidget(0, downloadWidget, 0, Qt.AlignTop)
        if self._num_downloads == 0:
            self._ui.m_zeroItemsLabel.hide()
        self._num_downloads += 1

    def remove(self, downloadWidget):
        self._ui.m_itemsLayout.removeWidget(downloadWidget)
        downloadWidget.deleteLater()
        self._num_downloads -= 1
        if self._num_downloads == 0:
            self._ui.m_zeroItemsLabel.show()
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>DownloadManagerWidget</class>
 <widget class="QWidget" name="DownloadManagerWidget">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>400</width>
    <height>212</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Downloads</string>
  </property>
  <property name="styleSheet">
   <string notr="true">#DownloadManagerWidget {
    background: palette(button)
}</string>
  </property>
  <layout class="QVBoxLayout" name="m_topLevelLayout">
   <property name="sizeConstraint">
    <enum>QLayout::SizeConstraint::SetNoConstraint</enum>
   </property>
   <property name="leftMargin">
    <number>0</number>
   </property>
   <property name="topMargin">
    <number>0</number>
   </property>
   <property name="rightMargin">
    <number>0</number>
   </property>
   <property name="bottomMargin">
    <number>0</number>
   </property>
   <item>
    <widget class="QScrollArea" name="m_scrollArea">
     <property name="styleSheet">
      <string notr="true">#m_scrollArea {
  margin: 2px;
  border: none;
}</string>
     </property>
     <property name="verticalScrollBarPolicy">
      <enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOn</enum>
     </property>
     <property name="horizontalScrollBarPolicy">
      <enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
     </property>
     <property name="widgetResizable">
      <bool>true</bool>
     </property>
     <property name="alignment">
      <set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
     </property>
     <widget class="QWidget" name="m_items">
      <property name="geometry">
       <rect>
        <x>0</x>
        <y>0</y>
        <width>382</width>
        <height>208</height>
       </rect>
      </property>
      <property name="styleSheet">
       <string notr="true">#m_items {background: palette(mid)}</string>
      </property>
      <layout class="QVBoxLayout" name="m_itemsLayout">
       <property name="spacing">
        <number>2</number>
       </property>
       <property name="leftMargin">
        <number>3</number>
       </property>
       <property name="topMargin">
        <number>3</number>
       </property>
       <property name="rightMargin">
        <number>3</number>
       </property>
       <property name="bottomMargin">
        <number>3</number>
       </property>
       <item>
        <widget class="QLabel" name="m_zeroItemsLabel">
         <property name="sizePolicy">
          <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
           <horstretch>0</horstretch>
           <verstretch>0</verstretch>
          </sizepolicy>
         </property>
         <property name="styleSheet">
          <string notr="true">color: palette(shadow)</string>
         </property>
         <property name="text">
          <string>No downloads</string>
         </property>
         <property name="alignment">
          <set>Qt::AlignmentFlag::AlignCenter</set>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
    </widget>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from ui_downloadwidget import Ui_DownloadWidget

from PySide6.QtWebEngineCore import QWebEngineDownloadRequest
from PySide6.QtWidgets import QFrame, QWidget
from PySide6.QtGui import QIcon
from PySide6.QtCore import QElapsedTimer, Signal, Slot


def with_unit(bytes):
    if bytes < (1 << 10):
        return f"{bytes} B"
    if bytes < (1 << 20):
        s = bytes / (1 << 10)
        return f"{int(s)} KiB"
    if bytes < (1 << 30):
        s = bytes / (1 << 20)
        return f"{int(s)} MiB"
    s = bytes / (1 << 30)
    return f"{int(s)} GiB"


class DownloadWidget(QFrame):
    """Displays one ongoing or finished download (QWebEngineDownloadRequest)."""

    # This signal is emitted when the user indicates that they want to remove
    # this download from the downloads list.
    remove_clicked = Signal(QWidget)

    def __init__(self, download, parent=None):
        super().__init__(parent)
        self._download = download
        self._time_added = QElapsedTimer()
        self._time_added.start()
        self._cancel_icon = QIcon.fromTheme(QIcon.ThemeIcon.ProcessStop,
                                            QIcon(":process-stop.png"))
        self._remove_icon = QIcon.fromTheme(QIcon.ThemeIcon.EditClear,
                                            QIcon(":edit-clear.png"))

        self._ui = Ui_DownloadWidget()
        self._ui.setupUi(self)
        self._ui.m_dstName.setText(self._download.downloadFileName())
        self._ui.m_srcUrl.setText(self._download.url().toDisplayString())

        self._ui.m_cancelButton.clicked.connect(self._canceled)

        self._download.totalBytesChanged.connect(self.update_widget)
        self._download.receivedBytesChanged.connect(self.update_widget)

        self._download.stateChanged.connect(self.update_widget)

        self.update_widget()

    @Slot()
    def _canceled(self):
        state = self._download.state()
        if state == QWebEngineDownloadRequest.DownloadInProgress:
            self._download.cancel()
        else:
            self.remove_clicked.emit(self)

    def update_widget(self):
        total_bytes_v = self._download.totalBytes()
        total_bytes = with_unit(total_bytes_v)
        received_bytes_v = self._download.receivedBytes()
        received_bytes = with_unit(received_bytes_v)
        elapsed = self._time_added.elapsed()
        bytes_per_second_v = received_bytes_v / elapsed * 1000 if elapsed else 0
        bytes_per_second = with_unit(bytes_per_second_v)

        state = self._download.state()

        progress_bar = self._ui.m_progressBar
        if state == QWebEngineDownloadRequest.DownloadInProgress:
            if total_bytes_v > 0:
                progress = round(100 * received_bytes_v / total_bytes_v)
                progress_bar.setValue(progress)
                progress_bar.setDisabled(False)
                fmt = f"%p% - {received_bytes} of {total_bytes} downloaded - {bytes_per_second}/s"
                progress_bar.setFormat(fmt)
            else:
                progress_bar.setValue(0)
                progress_bar.setDisabled(False)
                fmt = f"unknown size - {received_bytes} downloaded - {bytes_per_second}/s"
                progress_bar.setFormat(fmt)
        elif state == QWebEngineDownloadRequest.DownloadCompleted:
            progress_bar.setValue(100)
            progress_bar.setDisabled(True)
            fmt = f"completed - {received_bytes} downloaded - {bytes_per_second}/s"
            progress_bar.setFormat(fmt)
        elif state == QWebEngineDownloadRequest.DownloadCancelled:
            progress_bar.setValue(0)
            progress_bar.setDisabled(True)
            fmt = f"cancelled - {received_bytes} downloaded - {bytes_per_second}/s"
            progress_bar.setFormat(fmt)
        elif state == QWebEngineDownloadRequest.DownloadInterrupted:
            progress_bar.setValue(0)
            progress_bar.setDisabled(True)
            fmt = "interrupted: " + self._download.interruptReasonString()
            progress_bar.setFormat(fmt)

        if state == QWebEngineDownloadRequest.DownloadInProgress:
            self._ui.m_cancelButton.setIcon(self._cancel_icon)
            self._ui.m_cancelButton.setToolTip("Stop downloading")
        else:
            self._ui.m_cancelButton.setIcon(self._remove_icon)
            self._ui.m_cancelButton.setToolTip("Remove from list")
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>DownloadWidget</class>
 <widget class="QFrame" name="DownloadWidget">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>144</width>
    <height>103</height>
   </rect>
  </property>
  <property name="styleSheet">
   <string notr="true">#DownloadWidget {
  background: palette(button);
  border: 1px solid palette(dark);
  margin: 0px;
}</string>
  </property>
  <layout class="QGridLayout" name="m_topLevelLayout">
   <property name="sizeConstraint">
    <enum>QLayout::SizeConstraint::SetMinAndMaxSize</enum>
   </property>
   <item row="0" column="0">
    <widget class="QLabel" name="m_dstName">
     <property name="styleSheet">
      <string notr="true">font-weight: bold
</string>
     </property>
     <property name="text">
      <string>TextLabel</string>
     </property>
    </widget>
   </item>
   <item row="0" column="1">
    <widget class="QPushButton" name="m_cancelButton">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
       <horstretch>0</horstretch>
       <verstretch>0</verstretch>
      </sizepolicy>
     </property>
     <property name="styleSheet">
      <string notr="true">QPushButton {
  margin: 1px;
  border: none;
}
QPushButton:pressed {
  margin: none;
  border: 1px solid palette(shadow);
  background: palette(midlight);
}</string>
     </property>
     <property name="flat">
      <bool>false</bool>
     </property>
    </widget>
   </item>
   <item row="1" column="0" colspan="2">
    <widget class="QLabel" name="m_srcUrl">
     <property name="maximumSize">
      <size>
       <width>350</width>
       <height>16777215</height>
      </size>
     </property>
     <property name="styleSheet">
      <string notr="true"/>
     </property>
     <property name="text">
      <string>TextLabel</string>
     </property>
    </widget>
   </item>
   <item row="2" column="0" colspan="2">
    <widget class="QProgressBar" name="m_progressBar">
     <property name="styleSheet">
      <string notr="true">font-size: 12px</string>
     </property>
     <property name="value">
      <number>24</number>
     </property>
    </widget>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>PasswordDialog</class>
 <widget class="QDialog" name="PasswordDialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>399</width>
    <height>148</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Authentication Required</string>
  </property>
  <layout class="QGridLayout" name="gridLayout" columnstretch="0,0" columnminimumwidth="0,0">
   <item row="0" column="0">
    <widget class="QLabel" name="m_iconLabel">
     <property name="text">
      <string>Icon</string>
     </property>
     <property name="alignment">
      <set>Qt::AlignmentFlag::AlignCenter</set>
     </property>
    </widget>
   </item>
   <item row="0" column="1">
    <widget class="QLabel" name="m_infoLabel">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
       <horstretch>0</horstretch>
       <verstretch>0</verstretch>
      </sizepolicy>
     </property>
     <property name="text">
      <string>Info</string>
     </property>
     <property name="wordWrap">
      <bool>true</bool>
     </property>
    </widget>
   </item>
   <item row="1" column="0">
    <widget class="QLabel" name="userLabel">
     <property name="text">
      <string>Username:</string>
     </property>
    </widget>
   </item>
   <item row="1" column="1">
    <widget class="QLineEdit" name="m_userNameLineEdit"/>
   </item>
   <item row="2" column="0">
    <widget class="QLabel" name="passwordLabel">
     <property name="text">
      <string>Password:</string>
     </property>
    </widget>
   </item>
   <item row="2" column="1">
    <widget class="QLineEdit" name="m_passwordLineEdit">
     <property name="echoMode">
      <enum>QLineEdit::EchoMode::Password</enum>
     </property>
    </widget>
   </item>
   <item row="3" column="0" colspan="2">
    <widget class="QDialogButtonBox" name="buttonBox">
     <property name="orientation">
      <enum>Qt::Orientation::Horizontal</enum>
     </property>
     <property name="standardButtons">
      <set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
     </property>
    </widget>
   </item>
  </layout>
  <zorder>userLabel</zorder>
  <zorder>m_userNameLineEdit</zorder>
  <zorder>passwordLabel</zorder>
  <zorder>m_passwordLineEdit</zorder>
  <zorder>buttonBox</zorder>
  <zorder>m_iconLabel</zorder>
  <zorder>m_infoLabel</zorder>
 </widget>
 <resources/>
 <connections>
  <connection>
   <sender>buttonBox</sender>
   <signal>accepted()</signal>
   <receiver>PasswordDialog</receiver>
   <slot>accept()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>248</x>
     <y>254</y>
    </hint>
    <hint type="destinationlabel">
     <x>157</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
  <connection>
   <sender>buttonBox</sender>
   <signal>rejected()</signal>
   <receiver>PasswordDialog</receiver>
   <slot>reject()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>316</x>
     <y>260</y>
    </hint>
    <hint type="destinationlabel">
     <x>286</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
 </connections>
</ui>
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from functools import partial

from PySide6.QtWebEngineCore import (QWebEngineFindTextResult, QWebEnginePage)
from PySide6.QtWidgets import QLabel, QMenu, QTabBar, QTabWidget
from PySide6.QtGui import QCursor, QIcon, QKeySequence, QPixmap
from PySide6.QtCore import QUrl, Qt, Signal, Slot

from webpage import WebPage
from webview import WebView


class TabWidget(QTabWidget):
    link_hovered = Signal(str)
    load_progress = Signal(int)
    title_changed = Signal(str)
    url_changed = Signal(QUrl)
    fav_icon_changed = Signal(QIcon)
    web_action_enabled_changed = Signal(QWebEnginePage.WebAction, bool)
    dev_tools_requested = Signal(QWebEnginePage)
    find_text_finished = Signal(QWebEngineFindTextResult)

    def __init__(self, profile, parent):
        super().__init__(parent)
        self._profile = profile
        tab_bar = self.tabBar()
        tab_bar.setTabsClosable(True)
        tab_bar.setSelectionBehaviorOnRemove(QTabBar.SelectPreviousTab)
        tab_bar.setMovable(True)
        tab_bar.setContextMenuPolicy(Qt.CustomContextMenu)
        tab_bar.customContextMenuRequested.connect(self.handle_context_menu_requested)
        tab_bar.tabCloseRequested.connect(self.close_tab)
        tab_bar.tabBarDoubleClicked.connect(self._tabbar_double_clicked)
        self.setDocumentMode(True)
        self.setElideMode(Qt.ElideRight)

        self.currentChanged.connect(self.handle_current_changed)

        if profile.isOffTheRecord():
            icon = QLabel(self)
            pixmap = QPixmap(":ninja.png")
            icon.setPixmap(pixmap.scaledToHeight(tab_bar.height()))
            w = icon.pixmap().width()
            self.setStyleSheet(f"QTabWidget.tab-bar {{ left: {w}px; }}")

    @Slot(int)
    def _tabbar_double_clicked(self, index):
        if index == -1:
            self.create_tab()

    def handle_current_changed(self, index):
        if index != -1:
            view = self.web_view(index)
            if view.url():
                view.setFocus()
            self.title_changed.emit(view.title())
            self.load_progress.emit(view.load_progress())
            self.url_changed.emit(view.url())
            self.fav_icon_changed.emit(view.fav_icon())
            e = view.is_web_action_enabled(QWebEnginePage.Back)
            self.web_action_enabled_changed.emit(QWebEnginePage.Back, e)
            e = view.is_web_action_enabled(QWebEnginePage.Forward)
            self.web_action_enabled_changed.emit(QWebEnginePage.Forward, e)
            e = view.is_web_action_enabled(QWebEnginePage.Stop)
            self.web_action_enabled_changed.emit(QWebEnginePage.Stop, e)
            e = view.is_web_action_enabled(QWebEnginePage.Reload)
            self.web_action_enabled_changed.emit(QWebEnginePage.Reload, e)
        else:
            self.title_changed.emit("")
            self.load_progress.emit(0)
            self.url_changed.emit(QUrl())
            self.fav_icon_changed.emit(QIcon())
            self.web_action_enabled_changed.emit(QWebEnginePage.Back, False)
            self.web_action_enabled_changed.emit(QWebEnginePage.Forward, False)
            self.web_action_enabled_changed.emit(QWebEnginePage.Stop, False)
            self.web_action_enabled_changed.emit(QWebEnginePage.Reload, True)

    def handle_context_menu_requested(self, pos):
        menu = QMenu()
        menu.addAction("New &Tab", QKeySequence.AddTab, self.create_tab)
        index = self.tabBar().tabAt(pos)
        if index != -1:
            action = menu.addAction("Clone Tab")
            action.triggered.connect(partial(self.clone_tab, index))
            menu.addSeparator()
            action = menu.addAction("Close Tab")
            action.setShortcut(QKeySequence.Close)
            action.triggered.connect(partial(self.close_tab, index))
            action = menu.addAction("Close Other Tabs")
            action.triggered.connect(partial(self.close_other_tabs, index))
            menu.addSeparator()
            action = menu.addAction("Reload Tab")
            action.setShortcut(QKeySequence.Refresh)
            action.triggered.connect(partial(self.reload_tab, index))
        else:
            menu.addSeparator()

        menu.addAction("Reload All Tabs", self.reload_all_tabs)
        menu.exec(QCursor.pos())

    def current_web_view(self):
        return self.web_view(self.currentIndex())

    def web_view(self, index):
        return self.widget(index)

    def _title_changed(self, web_view, title):
        index = self.indexOf(web_view)
        if index != -1:
            self.setTabText(index, title)
            self.setTabToolTip(index, title)

        if self.currentIndex() == index:
            self.title_changed.emit(title)

    def _url_changed(self, web_view, url):
        index = self.indexOf(web_view)
        if index != -1:
            self.tabBar().setTabData(index, url)
        if self.currentIndex() == index:
            self.url_changed.emit(url)

    def _load_progress(self, web_view, progress):
        if self.currentIndex() == self.indexOf(web_view):
            self.load_progress.emit(progress)

    def _fav_icon_changed(self, web_view, icon):
        index = self.indexOf(web_view)
        if index != -1:
            self.setTabIcon(index, icon)
        if self.currentIndex() == index:
            self.fav_icon_changed.emit(icon)

    def _link_hovered(self, web_view, url):
        if self.currentIndex() == self.indexOf(web_view):
            self.link_hovered.emit(url)

    def _webaction_enabled_changed(self, webView, action, enabled):
        if self.currentIndex() == self.indexOf(webView):
            self.web_action_enabled_changed.emit(action, enabled)

    def _window_close_requested(self, webView):
        index = self.indexOf(webView)
        if webView.page().inspectedPage():
            self.window().close()
        elif index >= 0:
            self.close_tab(index)

    def _find_text_finished(self, webView, result):
        if self.currentIndex() == self.indexOf(webView):
            self.find_text_finished.emit(result)

    def setup_view(self, webView):
        web_page = webView.page()
        webView.titleChanged.connect(partial(self._title_changed, webView))
        webView.urlChanged.connect(partial(self._url_changed, webView))
        webView.loadProgress.connect(partial(self._load_progress, webView))
        web_page.linkHovered.connect(partial(self._link_hovered, webView))
        webView.fav_icon_changed.connect(partial(self._fav_icon_changed, webView))
        webView.web_action_enabled_changed.connect(partial(self._webaction_enabled_changed,
                                                           webView))
        web_page.windowCloseRequested.connect(partial(self._window_close_requested,
                                                      webView))
        webView.dev_tools_requested.connect(self.dev_tools_requested)
        web_page.findTextFinished.connect(partial(self._find_text_finished,
                                                  webView))

    def create_tab(self):
        web_view = self.create_background_tab()
        self.setCurrentWidget(web_view)
        return web_view

    def create_background_tab(self):
        web_view = WebView()
        web_page = WebPage(self._profile, web_view)
        web_view.set_page(web_page)
        self.setup_view(web_view)
        index = self.addTab(web_view, "(Untitled)")
        self.setTabIcon(index, web_view.fav_icon())
        # Workaround for QTBUG-61770
        web_view.resize(self.currentWidget().size())
        web_view.show()
        return web_view

    def reload_all_tabs(self):
        for i in range(0, self.count()):
            self.web_view(i).reload()

    def close_other_tabs(self, index):
        for i in range(index, self.count() - 1, -1):
            self.close_tab(i)
        for i in range(-1, index - 1, -1):
            self.close_tab(i)

    def close_tab(self, index):
        view = self.web_view(index)
        if view:
            has_focus = view.hasFocus()
            self.removeTab(index)
            if has_focus and self.count() > 0:
                self.current_web_view().setFocus()
            if self.count() == 0:
                self.create_tab()
            view.deleteLater()

    def clone_tab(self, index):
        view = self.web_view(index)
        if view:
            tab = self.create_tab()
            tab.setUrl(view.url())

    def set_url(self, url):
        view = self.current_web_view()
        if view:
            view.setUrl(url)
            view.setFocus()

    def trigger_web_page_action(self, action):
        web_view = self.current_web_view()
        if web_view:
            web_view.triggerPageAction(action)
            web_view.setFocus()

    def next_tab(self):
        next = self.currentIndex() + 1
        if next == self.count():
            next = 0
        self.setCurrentIndex(next)

    def previous_tab(self):
        next = self.currentIndex() - 1
        if next < 0:
            next = self.count() - 1
        self.setCurrentIndex(next)

    def reload_tab(self, index):
        view = self.web_view(index)
        if view:
            view.reload()
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from functools import partial

from PySide6.QtWebEngineCore import QWebEnginePage, QWebEngineCertificateError
from PySide6.QtCore import QTimer, Signal


class WebPage(QWebEnginePage):

    create_certificate_error_dialog = Signal(QWebEngineCertificateError)

    def __init__(self, profile, parent):
        super().__init__(profile, parent)

        self.selectClientCertificate.connect(self.handle_select_client_certificate)
        self.certificateError.connect(self.handle_certificate_error)

    def _emit_create_certificate_error_dialog(self, error):
        self.create_certificate_error_dialog.emit(error)

    def handle_certificate_error(self, error):
        error.defer()
        QTimer.singleShot(0, partial(self._emit_create_certificate_error_dialog, error))

    def handle_select_client_certificate(self, selection):
        # Just select one.
        selection.select(selection.certificates()[0])
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtWidgets import QLineEdit, QSizePolicy, QWidget, QVBoxLayout
from PySide6.QtGui import QAction
from PySide6.QtCore import QUrl, Qt, Slot

from webpage import WebPage


class WebPopupWindow(QWidget):

    def __init__(self, view, profile, parent=None):
        super().__init__(parent, Qt.Window)
        self.m_urlLineEdit = QLineEdit(self)
        self._url_line_edit = QLineEdit()
        self._fav_action = QAction(self)
        self._view = view

        self.setAttribute(Qt.WA_DeleteOnClose)
        self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self._url_line_edit)
        layout.addWidget(self._view)

        self._view.setPage(WebPage(profile, self._view))
        self._view.setFocus()

        self._url_line_edit.setReadOnly(True)
        self._url_line_edit.addAction(self._fav_action, QLineEdit.LeadingPosition)

        self._view.titleChanged.connect(self.setWindowTitle)
        self._view.urlChanged.connect(self._url_changed)
        self._view.fav_icon_changed.connect(self._fav_action.setIcon)
        p = self._view.page()
        p.geometryChangeRequested.connect(self.handle_geometry_change_requested)
        p.windowCloseRequested.connect(self.close)

    @Slot(QUrl)
    def _url_changed(self, url):
        self._url_line_edit.setText(url.toDisplayString())

    def view(self):
        return self._view

    def handle_geometry_change_requested(self, newGeometry):
        window = self.windowHandle()
        if window:
            self.setGeometry(newGeometry.marginsRemoved(window.frameMargins()))
        self.show()
        self._view.setFocus()
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from functools import partial

from PySide6.QtWebEngineCore import (QWebEngineFileSystemAccessRequest,
                                     QWebEnginePage)
from PySide6.QtWebEngineWidgets import QWebEngineView

from PySide6.QtWidgets import QDialog, QMessageBox, QStyle
from PySide6.QtGui import QIcon
from PySide6.QtNetwork import QAuthenticator
from PySide6.QtCore import QTimer, Signal, Slot

from webpage import WebPage
from webpopupwindow import WebPopupWindow
from ui_passworddialog import Ui_PasswordDialog
from ui_certificateerrordialog import Ui_CertificateErrorDialog


def question_for_feature(feature):
    if feature == QWebEnginePage.Geolocation:
        return "Allow %1 to access your location information?"
    if feature == QWebEnginePage.MediaAudioCapture:
        return "Allow %1 to access your microphone?"
    if feature == QWebEnginePage.MediaVideoCapture:
        return "Allow %1 to access your webcam?"
    if feature == QWebEnginePage.MediaAudioVideoCapture:
        return "Allow %1 to access your microphone and webcam?"
    if feature == QWebEnginePage.MouseLock:
        return "Allow %1 to lock your mouse cursor?"
    if feature == QWebEnginePage.DesktopVideoCapture:
        return "Allow %1 to capture video of your desktop?"
    if feature == QWebEnginePage.DesktopAudioVideoCapture:
        return "Allow %1 to capture audio and video of your desktop?"
    if feature == QWebEnginePage.Notifications:
        return "Allow %1 to show notification on your desktop?"
    return ""


class WebView(QWebEngineView):

    web_action_enabled_changed = Signal(QWebEnginePage.WebAction, bool)
    fav_icon_changed = Signal(QIcon)
    dev_tools_requested = Signal(QWebEnginePage)

    def __init__(self, parent=None):
        super().__init__(parent)

        self._load_progress = 100
        self.loadStarted.connect(self._load_started)
        self.loadProgress.connect(self._slot_load_progress)
        self.loadFinished.connect(self._load_finished)
        self.iconChanged.connect(self._emit_faviconchanged)
        self.renderProcessTerminated.connect(self._render_process_terminated)

        self._error_icon = QIcon(":dialog-error.png")
        self._loading_icon = QIcon.fromTheme(QIcon.ThemeIcon.ViewRefresh,
                                             QIcon(":view-refresh.png"))
        self._default_icon = QIcon(":text-html.png")

    @Slot()
    def _load_started(self):
        self._load_progress = 0
        self.fav_icon_changed.emit(self.fav_icon())

    @Slot(int)
    def _slot_load_progress(self, progress):
        self._load_progress = progress

    @Slot()
    def _emit_faviconchanged(self):
        self.fav_icon_changed.emit(self.fav_icon())

    @Slot(bool)
    def _load_finished(self, success):
        self._load_progress = 100 if success else -1
        self._emit_faviconchanged()

    @Slot(QWebEnginePage.RenderProcessTerminationStatus, int)
    def _render_process_terminated(self, termStatus, statusCode):
        status = ""
        if termStatus == QWebEnginePage.NormalTerminationStatus:
            status = "Render process normal exit"
        elif termStatus == QWebEnginePage.AbnormalTerminationStatus:
            status = "Render process abnormal exit"
        elif termStatus == QWebEnginePage.CrashedTerminationStatus:
            status = "Render process crashed"
        elif termStatus == QWebEnginePage.KilledTerminationStatus:
            status = "Render process killed"

        m = f"Render process exited with code: {statusCode:#x}\nDo you want to reload the page?"
        btn = QMessageBox.question(self.window(), status, m)
        if btn == QMessageBox.Yes:
            QTimer.singleShot(0, self.reload)

    def set_page(self, page):
        old_page = self.page()
        if old_page and isinstance(old_page, WebPage):
            old_page.createCertificateErrorDialog.disconnect(self.handle_certificate_error)
            old_page.authenticationRequired.disconnect(self.handle_authentication_required)
            old_page.featurePermissionRequested.disconnect(self.handle_feature_permission_requested)
            old_page.proxyAuthenticationRequired.disconnect(
                self.handle_proxy_authentication_required)
            old_page.registerProtocolHandlerRequested.disconnect(
                self.handle_register_protocol_handler_requested)
            old_page.fileSystemAccessRequested.disconnect(self.handle_file_system_access_requested)

        self.create_web_action_trigger(page, QWebEnginePage.Forward)
        self.create_web_action_trigger(page, QWebEnginePage.Back)
        self.create_web_action_trigger(page, QWebEnginePage.Reload)
        self.create_web_action_trigger(page, QWebEnginePage.Stop)
        super().setPage(page)
        page.create_certificate_error_dialog.connect(self.handle_certificate_error)
        page.authenticationRequired.connect(self.handle_authentication_required)
        page.featurePermissionRequested.connect(self.handle_feature_permission_requested)
        page.proxyAuthenticationRequired.connect(self.handle_proxy_authentication_required)
        page.registerProtocolHandlerRequested.connect(
            self.handle_register_protocol_handler_requested)
        page.fileSystemAccessRequested.connect(self.handle_file_system_access_requested)

    def load_progress(self):
        return self._load_progress

    def _emit_webactionenabledchanged(self, action, webAction):
        self.web_action_enabled_changed.emit(webAction, action.isEnabled())

    def create_web_action_trigger(self, page, webAction):
        action = page.action(webAction)
        action.changed.connect(partial(self._emit_webactionenabledchanged, action, webAction))

    def is_web_action_enabled(self, webAction):
        return self.page().action(webAction).isEnabled()

    def fav_icon(self):
        fav_icon = self.icon()
        if not fav_icon.isNull():
            return fav_icon
        if self._load_progress < 0:
            return self._error_icon
        if self._load_progress < 100:
            return self._loading_icon
        return self._default_icon

    def createWindow(self, type):
        main_window = self.window()
        if not main_window:
            return None

        if type == QWebEnginePage.WebBrowserTab:
            return main_window.tab_widget().create_tab()

        if type == QWebEnginePage.WebBrowserBackgroundTab:
            return main_window.tab_widget().create_background_tab()

        if type == QWebEnginePage.WebBrowserWindow:
            return main_window.browser().createWindow().current_tab()

        if type == QWebEnginePage.WebDialog:
            view = WebView()
            WebPopupWindow(view, self.page().profile(), self.window())
            view.dev_tools_requested.connect(self.dev_tools_requested)
            return view

        return None

    @Slot()
    def _emit_devtools_requested(self):
        self.dev_tools_requested.emit(self.page())

    def contextMenuEvent(self, event):
        menu = self.createStandardContextMenu()
        actions = menu.actions()
        inspect_action = self.page().action(QWebEnginePage.InspectElement)
        if inspect_action in actions:
            inspect_action.setText("Inspect element")
        else:
            vs = self.page().action(QWebEnginePage.ViewSource)
            if vs not in actions:
                menu.addSeparator()

            action = menu.addAction("Open inspector in new window")
            action.triggered.connect(self._emit_devtools_requested)

        menu.popup(event.globalPos())

    def handle_certificate_error(self, error):
        w = self.window()
        dialog = QDialog(w)
        dialog.setModal(True)

        certificate_dialog = Ui_CertificateErrorDialog()
        certificate_dialog.setupUi(dialog)
        certificate_dialog.m_iconLabel.setText("")
        icon = QIcon(w.style().standardIcon(QStyle.SP_MessageBoxWarning, 0, w))
        certificate_dialog.m_iconLabel.setPixmap(icon.pixmap(32, 32))
        certificate_dialog.m_errorLabel.setText(error.description())
        dialog.setWindowTitle("Certificate Error")

        if dialog.exec() == QDialog.Accepted:
            error.acceptCertificate()
        else:
            error.rejectCertificate()

    def handle_authentication_required(self, requestUrl, auth):
        w = self.window()
        dialog = QDialog(w)
        dialog.setModal(True)

        password_dialog = Ui_PasswordDialog()
        password_dialog.setupUi(dialog)

        password_dialog.m_iconLabel.setText("")
        icon = QIcon(w.style().standardIcon(QStyle.SP_MessageBoxQuestion, 0, w))
        password_dialog.m_iconLabel.setPixmap(icon.pixmap(32, 32))

        url_str = requestUrl.toString().toHtmlEscaped()
        realm = auth.realm()
        m = f'Enter username and password for "{realm}" at {url_str}'
        password_dialog.m_infoLabel.setText(m)
        password_dialog.m_infoLabel.setWordWrap(True)

        if dialog.exec() == QDialog.Accepted:
            auth.setUser(password_dialog.m_userNameLineEdit.text())
            auth.setPassword(password_dialog.m_passwordLineEdit.text())
        else:
            # Set authenticator null if dialog is cancelled
            auth = QAuthenticator()

    def handle_feature_permission_requested(self, securityOrigin, feature):
        title = "Permission Request"
        host = securityOrigin.host()
        question = question_for_feature(feature).replace("%1", host)
        w = self.window()
        page = self.page()
        if question and QMessageBox.question(w, title, question) == QMessageBox.Yes:
            page.setFeaturePermission(securityOrigin, feature,
                                      QWebEnginePage.PermissionGrantedByUser)
        else:
            page.setFeaturePermission(securityOrigin, feature,
                                      QWebEnginePage.PermissionDeniedByUser)

    def handle_proxy_authentication_required(self, url, auth, proxyHost):
        w = self.window()
        dialog = QDialog(w)
        dialog.setModal(True)

        password_dialog = Ui_PasswordDialog()
        password_dialog.setupUi(dialog)

        password_dialog.m_iconLabel.setText("")

        icon = QIcon(w.style().standardIcon(QStyle.SP_MessageBoxQuestion, 0, w))
        password_dialog.m_iconLabel.setPixmap(icon.pixmap(32, 32))

        proxy = proxyHost.toHtmlEscaped()
        password_dialog.m_infoLabel.setText(f'Connect to proxy "{proxy}" using:')
        password_dialog.m_infoLabel.setWordWrap(True)

        if dialog.exec() == QDialog.Accepted:
            auth.setUser(password_dialog.m_userNameLineEdit.text())
            auth.setPassword(password_dialog.m_passwordLineEdit.text())
        else:
            # Set authenticator null if dialog is cancelled
            auth = QAuthenticator()

    def handle_register_protocol_handler_requested(self, request):
        host = request.origin().host()
        m = f"Allow {host} to open all {request.scheme()} links?"
        answer = QMessageBox.question(self.window(), "Permission Request", m)
        if answer == QMessageBox.Yes:
            request.accept()
        else:
            request.reject()

    def handle_file_system_access_requested(self, request):
        access_type = ""
        type = request.accessFlags()
        if type == QWebEngineFileSystemAccessRequest.Read:
            access_type = "read"
        elif type == QWebEngineFileSystemAccessRequest.Write:
            access_type = "write"
        elif type == (QWebEngineFileSystemAccessRequest.Read
                      | QWebEngineFileSystemAccessRequest.Write):
            access_type = "read and write"
        host = request.origin().host()
        path = request.filePath().toString()
        t = "File system access request"
        m = f"Give {host} {access_type} access to {path}?"
        answer = QMessageBox.question(self.window(), t, m)
        if answer == QMessageBox.Yes:
            request.accept()
        else:
            request.reject()