简单浏览器¶
简单浏览器演示了如何使用Qt WebEngine Widgets类来开发一个包含以下元素的小型Web浏览器应用程序:
用于打开已保存页面和管理窗口和标签页的菜单栏。
导航栏用于输入URL并在网页浏览历史中前进和后退。
多标签区域,用于在标签页内显示网页内容。
用于显示悬停链接的状态栏。
一个简单的下载管理器。
网页内容可以在新标签页或单独的窗口中打开。HTTP和代理认证可以用于访问网页。
类层次结构¶
我们将实现以下主要类:
Browser
是一个管理应用程序窗口的类。BrowserWindow
是一个QMainWindow
,显示菜单、导航栏栏,
TabWidget
,和状态栏。
TabWidget
是一个QTabWidget
并且包含一个或多个浏览器标签。
WebView
是一个QWebEngineView
,为WebPage
提供视图,并作为标签页添加到
TabWidget
中。
WebPage
是一个QWebEnginePage
,表示网站内容。
此外,我们将实现一些辅助类:
WebPopupWindow
是一个用于显示弹出窗口的QWidget
。DownloadManagerWidget
是一个实现下载的QWidget
列表。
创建浏览器主窗口¶
此示例支持由Browser
对象拥有的多个主窗口。此类还拥有DownloadManagerWidget
,并可用于进一步的功能,例如书签和历史记录管理器。
在main.cpp
中,我们创建了第一个BrowserWindow
实例并将其添加到Browser
对象中。如果命令行没有传递任何参数,我们将打开Qt主页。
为了在将窗口切换到OpenGL渲染时抑制闪烁,我们在添加第一个浏览器标签后调用show。
创建标签¶
BrowserWindow
构造函数初始化所有必要的用户界面相关对象。BrowserWindow
的 centralWidget 包含一个 TabWidget
的实例。TabWidget
包含一个或多个 WebView
实例作为标签页,并将其信号和槽委托给当前选中的标签页。
在TabWidget.setup_view()
中,我们确保TabWidget
始终转发当前选中的WebView
的信号。
实现WebView功能¶
类 WebView
继承自 QWebEngineView
以支持以下功能:
在渲染进程崩溃时显示错误消息
处理
createWindow()
请求向上下文菜单添加自定义菜单项
管理WebWindows¶
加载的页面可能希望创建类型为
QWebEnginePage.WebWindowType
的窗口,例如,当JavaScript程序请求
在新窗口或对话框中打开文档时。这是通过重写
QWebView.createWindow()
来处理的。
在QWebEnginePage.WebDialog
的情况下,我们创建了一个自定义的WebPopupWindow
类的实例。
实现网页和WebView功能¶
我们将WebPage
实现为QWebEnginePage
的子类,并将WebView
实现为QWebEngineView
的子类,以启用HTTP、代理认证以及在访问网页时忽略SSL证书错误。
在上述所有情况下,我们向用户显示适当的对话框。在身份验证的情况下,我们需要在QAuthenticator对象上设置正确的凭据值。
handleProxyAuthenticationRequired
信号处理程序实现了与HTTP代理认证相同的步骤。
在SSL错误的情况下,我们只需要返回一个布尔值,指示是否应忽略证书。
打开网页¶
本节描述了打开新页面的工作流程。当用户在导航栏中输入URL并按下回车键时,会发出QLineEdit.:returnPressed()
信号,然后将新的URL传递给TabWidget.set_url()
。
呼叫被转接到当前选定的标签页。
set_url()
方法属于 WebView
,它只是将 URL 转发给关联的 WebPage
,后者随后在后台开始下载页面的内容。
实现隐私浏览¶
隐私浏览、隐身模式或无痕模式是许多浏览器的一项功能,其中通常持久化的数据,如cookies、HTTP缓存或浏览历史记录,仅保存在内存中,不会在磁盘上留下痕迹。在这个例子中,我们将在窗口级别实现隐私浏览,其中一个窗口中的所有标签页都处于正常模式或隐私模式。或者,我们可以在标签页级别实现隐私浏览,其中窗口中的一些标签页处于正常模式,其他标签页处于隐私模式。
使用Qt WebEngine实现隐私浏览非常简单。只需要创建一个新的QWebEngineProfile
并在QWebEnginePage
中使用它,而不是默认的配置文件。在示例中,这个新的配置文件由Browser
对象拥有。
为私人浏览所需的配置文件与其第一个窗口一起创建。QWebEngineProfile
的默认构造函数已经将其置于无痕模式。
剩下的就是将适当的配置文件传递给相应的QWebEnginePage
对象。Browser
对象将向每个新的BrowserWindow
传递全局默认配置文件或一个共享的off-the-record配置文件实例。
然后,BrowserWindow
和 TabWidget
对象将确保窗口中的所有
QWebEnginePage
对象都将使用此配置文件。
管理下载¶
下载与QWebEngineProfile
相关联。每当在网页上触发下载时,QWebEngineProfile.downloadRequested
信号会发出一个QWebEngineDownloadRequest
,在这个例子中,它被转发到DownloadManagerWidget.download_requested()
。
此方法提示用户输入文件名(带有预填建议)并开始下载(除非用户取消保存 为
对话框)。
QWebEngineDownloadRequest
对象会定期发出
QWebEngineDownloadRequest.receivedBytesChanged()
信号,以通知潜在的观察者下载进度,并在下载完成或发生错误时发出
QWebEngineDownloadRequest.stateChanged()
信号。
文件和归属¶
该示例使用了来自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()