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 aQMainWindow
showing the menu, a navigationbar,
TabWidget
, and a status bar.
TabWidget
is aQTabWidget
and contains one or multiplebrowser tabs.
WebView
is aQWebEngineView
, provides a view forWebPage
,and is added as a tab in
TabWidget
.
WebPage
is aQWebEnginePage
that represents website content.
Additionally, we will implement some auxiliary classes:
WebPopupWindow
is aQWidget
for showing popup windows.DownloadManagerWidget
is aQWidget
implementing the downloadslist.
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()
requestsAdding 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.
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.
# 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()