简单浏览器#
简单浏览器演示了如何使用 Qt WebEngine 小部件类来开发一个小型 Web 浏览器应用程序,该应用程序包含以下元素:
菜单栏,用于打开已存储的页面以及管理窗口和标签页。
导航栏,用于输入 URL 以进入网页,以及在网页浏览历史中向前和向后导航。
多标签页区域,用于在标签页内显示 Web 内容。
状态栏,用于显示悬停链接。
一个简单的下载管理器。
Web 内容可以在新标签页或单独的窗口中打开。可以使用 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()》请求
向上下文菜单添加自定义菜单项
管理Web窗口#
加载的页面可能想要创建类型为《QWebEnginePage.WebWindowType
》的窗口,例如,当一个JavaScript程序请求在新窗口或对话框中打开一个文档时。这通过重写《QWebView.createWindow()`》来处理。
对于《QWebEnginePage.WebDialog》的情况,我们创建一个自定义的《WebPopupWindow》类的实例。
实现 WebPage 和 WebView 功能#
我们将 WebPage
实现为 QWebEnginePage
的子类,将 WebView
实现为 QWebEngineView
的子类,以启用 HTTP、代理身份验证,以及在访问网页时忽略 SSL 证书错误。
在所有上述情况下,我们都向用户显示适当的对话框。在身份验证的情况下,我们需要在 QAuthenticator
对象上设置正确的凭据值。
handleProxyAuthenticationRequired
信号处理程序为 HTTP 代理的身份验证实现了相同的步骤。
在 SSL 错误的情况下,我们只需返回一个表示证书是否应被忽略的布尔值。
打开网页#
本节描述了打开新页面的工作流程。当用户在导航栏中输入 URL 并按 Enter 键时,将发出 QLineEdit.:returnPressed()
信号,然后将新的 URL 交给 TabWidget.set_url()
。
调用将转发到当前选中的标签页。
WebView
的 set_url()
方法仅将 URL 转发给关联的 WebPage
,然后它再转而在后台开始下载网页内容。
实现私密浏览#
私密浏览、隐身模式或无记录模式是许多浏览器的功能,其中通常持久的资料,如 Cookie、HTTP 缓存或浏览历史,只保留在内存中,不会在磁盘上留下痕迹。在此示例中,我们将以窗口级别实现私密浏览,一个窗口中的所有标签页都在正常或私密模式中。或者我们也可以在标签页级别实现私密浏览,在某些窗口中的标签页以正常模式打开,而在其他标签页以私密模式打开。
使用 Qt WebEngine 实现私密浏览非常简单。只需要创建一个新的 QWebEngineProfile
并在 QWebEnginePage
中使用它而不是默认配置文件。在示例中,此新配置文件由 Browser
对象拥有。
为 私密浏览 所需的配置文件与其第一个窗口一起创建。 QWebEngineProfile
的默认构造函数已经将其置于 无记录 模式。
所有剩下的工作就是将合适的配置文件向下传递到相应的 QWebEnginePage
对象中。Browser
对象将会将全局默认配置文件或一个共享的 离线记录 配置文件实例传递给每个新的 BrowserWindow
对象。
接着,BrowserWindow
和 TabWidget
对象会确保一个窗口中包含的所有 QWebEnginePage
对象都使用这个配置文件。
管理下载#
下载与 QWebEngineProfile
相关联。每当在网页上触发下载时,QWebEngineProfile.downloadRequested
信号会发出,携带一个 QWebEngineDownloadRequest
,在本例中,它被转发到 DownloadManagerWidget.download_requested()
。
该方法会提示用户输入文件名(带有预填充的建议)并开始下载(除非用户取消 保存为
对话框)。
QWebEngineDownloadRequest
对象会定期发出 QWebEngineDownloadRequest.receivedBytesChanged()
信号以通知潜在的观察者下载进度,并在下载完成或出现错误时发出 QWebEngineDownloadRequest.stateChanged()
信号。
文件和归属#
该示例使用了来自 Tango 图标库 的图标。
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
"""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 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
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>370</width>
<height>141</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::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::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::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::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::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::No|QDialogButtonBox::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 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::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::ScrollBarAlwaysOn</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<widget class="QWidget" name="m_items">
<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::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 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="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::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"/>
</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::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::Password</enum>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::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 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 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 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 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()