351 lines
14 KiB
Python
351 lines
14 KiB
Python
# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
"""The main browser widget for QtWebEngine."""
|
|
|
|
import mimetypes
|
|
from typing import List, Iterable, Optional
|
|
|
|
from qutebrowser.qt import machinery
|
|
from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QUrl
|
|
from qutebrowser.qt.gui import QPalette
|
|
from qutebrowser.qt.webenginewidgets import QWebEngineView
|
|
from qutebrowser.qt.webenginecore import (
|
|
QWebEnginePage, QWebEngineCertificateError, QWebEngineSettings,
|
|
QWebEngineHistory,
|
|
)
|
|
|
|
from qutebrowser.browser import shared
|
|
from qutebrowser.browser.webengine import webenginesettings, certificateerror
|
|
from qutebrowser.config import config
|
|
from qutebrowser.utils import log, debug, usertypes, qtutils
|
|
|
|
|
|
_QB_FILESELECTION_MODES = {
|
|
QWebEnginePage.FileSelectionMode.FileSelectOpen: shared.FileSelectionMode.single_file,
|
|
QWebEnginePage.FileSelectionMode.FileSelectOpenMultiple: shared.FileSelectionMode.multiple_files,
|
|
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-91489
|
|
#
|
|
# QtWebEngine doesn't expose this value from its internal
|
|
# FilePickerControllerPrivate::FileChooserMode enum (i.e. it's not included in
|
|
# the public QWebEnginePage::FileSelectionMode enum).
|
|
# However, QWebEnginePage::chooseFiles is still called with the matching value
|
|
# (2) when a file input with "webkitdirectory" is used.
|
|
QWebEnginePage.FileSelectionMode(2): shared.FileSelectionMode.folder,
|
|
}
|
|
|
|
|
|
class WebEngineView(QWebEngineView):
|
|
|
|
"""Custom QWebEngineView subclass with qutebrowser-specific features."""
|
|
|
|
def __init__(self, *, tabdata, win_id, private, parent=None):
|
|
super().__init__(parent)
|
|
self._win_id = win_id
|
|
self._tabdata = tabdata
|
|
|
|
style = self.style()
|
|
assert style is not None
|
|
theme_color = style.standardPalette().color(QPalette.ColorRole.Base)
|
|
if private:
|
|
assert webenginesettings.private_profile is not None
|
|
profile = webenginesettings.private_profile
|
|
assert profile.isOffTheRecord()
|
|
else:
|
|
profile = webenginesettings.default_profile
|
|
page = WebEnginePage(theme_color=theme_color, profile=profile,
|
|
parent=self)
|
|
self.setPage(page)
|
|
|
|
def render_widget(self):
|
|
"""Get the RenderWidgetHostViewQt for this view."""
|
|
return self.focusProxy()
|
|
|
|
def shutdown(self):
|
|
"""Shut down the underlying page."""
|
|
page = self.page()
|
|
assert isinstance(page, WebEnginePage), page
|
|
page.shutdown()
|
|
|
|
def createWindow(self, wintype):
|
|
"""Called by Qt when a page wants to create a new window.
|
|
|
|
This function is called from the createWindow() method of the
|
|
associated QWebEnginePage, each time the page wants to create a new
|
|
window of the given type. This might be the result, for example, of a
|
|
JavaScript request to open a document in a new window.
|
|
|
|
Args:
|
|
wintype: This enum describes the types of window that can be
|
|
created by the createWindow() function.
|
|
|
|
QWebEnginePage::WebBrowserWindow:
|
|
A complete web browser window.
|
|
QWebEnginePage::WebBrowserTab:
|
|
A web browser tab.
|
|
QWebEnginePage::WebDialog:
|
|
A window without decoration.
|
|
QWebEnginePage::WebBrowserBackgroundTab:
|
|
A web browser tab without hiding the current visible
|
|
WebEngineView.
|
|
|
|
Return:
|
|
The new QWebEngineView object.
|
|
"""
|
|
debug_type = debug.qenum_key(QWebEnginePage, wintype)
|
|
background = config.val.tabs.background
|
|
|
|
log.webview.debug("createWindow with type {}, background {}".format(
|
|
debug_type, background))
|
|
|
|
if wintype == QWebEnginePage.WebWindowType.WebBrowserWindow:
|
|
# Shift-Alt-Click
|
|
target = usertypes.ClickTarget.window
|
|
elif wintype == QWebEnginePage.WebWindowType.WebDialog:
|
|
log.webview.warning("{} requested, but we don't support "
|
|
"that!".format(debug_type))
|
|
target = usertypes.ClickTarget.tab
|
|
elif wintype == QWebEnginePage.WebWindowType.WebBrowserTab:
|
|
# Middle-click / Ctrl-Click with Shift
|
|
# FIXME:qtwebengine this also affects target=_blank links...
|
|
if background:
|
|
target = usertypes.ClickTarget.tab
|
|
else:
|
|
target = usertypes.ClickTarget.tab_bg
|
|
elif wintype == QWebEnginePage.WebWindowType.WebBrowserBackgroundTab:
|
|
# Middle-click / Ctrl-Click
|
|
if background:
|
|
target = usertypes.ClickTarget.tab_bg
|
|
else:
|
|
target = usertypes.ClickTarget.tab
|
|
else:
|
|
raise ValueError("Invalid wintype {}".format(debug_type))
|
|
|
|
tab = shared.get_tab(self._win_id, target)
|
|
return tab._widget # pylint: disable=protected-access
|
|
|
|
def contextMenuEvent(self, ev):
|
|
"""Prevent context menus when rocker gestures are enabled."""
|
|
if config.val.input.mouse.rocker_gestures:
|
|
ev.ignore()
|
|
return
|
|
super().contextMenuEvent(ev)
|
|
|
|
def page(self) -> "WebEnginePage":
|
|
"""Return the page for this view."""
|
|
maybe_page = super().page()
|
|
assert maybe_page is not None
|
|
assert isinstance(maybe_page, WebEnginePage)
|
|
return maybe_page
|
|
|
|
def settings(self) -> "QWebEngineSettings":
|
|
"""Return the settings for this view."""
|
|
maybe_settings = super().settings()
|
|
assert maybe_settings is not None
|
|
return maybe_settings
|
|
|
|
def history(self) -> "QWebEngineHistory":
|
|
"""Return the history for this view."""
|
|
maybe_history = super().history()
|
|
assert maybe_history is not None
|
|
return maybe_history
|
|
|
|
|
|
def extra_suffixes_workaround(upstream_mimetypes):
|
|
"""Return any extra suffixes for mimetypes in upstream_mimetypes.
|
|
|
|
Return any file extensions (aka suffixes) for mimetypes listed in
|
|
upstream_mimetypes that are not already contained in there.
|
|
|
|
WORKAROUND: for https://bugreports.qt.io/browse/QTBUG-116905
|
|
Affected Qt versions > 6.2.2 (probably) < 6.7.0
|
|
"""
|
|
if not (
|
|
qtutils.version_check("6.2.3", compiled=False)
|
|
and not qtutils.version_check("6.7.0", compiled=False)
|
|
):
|
|
return set()
|
|
|
|
suffixes = {entry for entry in upstream_mimetypes if entry.startswith(".")}
|
|
mimes = {entry for entry in upstream_mimetypes if "/" in entry}
|
|
python_suffixes = set()
|
|
for mime in mimes:
|
|
if mime.endswith("/*"):
|
|
python_suffixes.update(
|
|
[
|
|
suffix
|
|
for suffix, mimetype in mimetypes.types_map.items()
|
|
if mimetype.startswith(mime[:-1])
|
|
]
|
|
)
|
|
else:
|
|
python_suffixes.update(mimetypes.guess_all_extensions(mime))
|
|
return python_suffixes - suffixes
|
|
|
|
|
|
class WebEnginePage(QWebEnginePage):
|
|
|
|
"""Custom QWebEnginePage subclass with qutebrowser-specific features.
|
|
|
|
Attributes:
|
|
_is_shutting_down: Whether the page is currently shutting down.
|
|
_theme_color: The theme background color.
|
|
|
|
Signals:
|
|
certificate_error: Emitted on certificate errors.
|
|
Needs to be directly connected to a slot calling
|
|
.accept_certificate(), .reject_certificate, or
|
|
.defer().
|
|
shutting_down: Emitted when the page is shutting down.
|
|
navigation_request: Emitted on acceptNavigationRequest.
|
|
"""
|
|
|
|
certificate_error = pyqtSignal(certificateerror.CertificateErrorWrapper)
|
|
shutting_down = pyqtSignal()
|
|
navigation_request = pyqtSignal(usertypes.NavigationRequest)
|
|
|
|
_JS_LOG_LEVEL_MAPPING = {
|
|
QWebEnginePage.JavaScriptConsoleMessageLevel.InfoMessageLevel:
|
|
usertypes.JsLogLevel.info,
|
|
QWebEnginePage.JavaScriptConsoleMessageLevel.WarningMessageLevel:
|
|
usertypes.JsLogLevel.warning,
|
|
QWebEnginePage.JavaScriptConsoleMessageLevel.ErrorMessageLevel:
|
|
usertypes.JsLogLevel.error,
|
|
}
|
|
|
|
_NAVIGATION_TYPE_MAPPING = {
|
|
QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
|
|
usertypes.NavigationRequest.Type.link_clicked,
|
|
QWebEnginePage.NavigationType.NavigationTypeTyped:
|
|
usertypes.NavigationRequest.Type.typed,
|
|
QWebEnginePage.NavigationType.NavigationTypeFormSubmitted:
|
|
usertypes.NavigationRequest.Type.form_submitted,
|
|
QWebEnginePage.NavigationType.NavigationTypeBackForward:
|
|
usertypes.NavigationRequest.Type.back_forward,
|
|
QWebEnginePage.NavigationType.NavigationTypeReload:
|
|
usertypes.NavigationRequest.Type.reload,
|
|
QWebEnginePage.NavigationType.NavigationTypeOther:
|
|
usertypes.NavigationRequest.Type.other,
|
|
QWebEnginePage.NavigationType.NavigationTypeRedirect:
|
|
usertypes.NavigationRequest.Type.redirect,
|
|
}
|
|
|
|
def __init__(self, *, theme_color, profile, parent=None):
|
|
super().__init__(profile, parent)
|
|
self._is_shutting_down = False
|
|
self._theme_color = theme_color
|
|
self._set_bg_color()
|
|
config.instance.changed.connect(self._set_bg_color)
|
|
if machinery.IS_QT6:
|
|
self.certificateError.connect(self._handle_certificate_error)
|
|
# Qt 5: Overridden method instead of signal
|
|
|
|
@config.change_filter('colors.webpage.bg')
|
|
def _set_bg_color(self):
|
|
col = config.val.colors.webpage.bg
|
|
if col is None:
|
|
col = self._theme_color
|
|
self.setBackgroundColor(col)
|
|
|
|
def shutdown(self):
|
|
self._is_shutting_down = True
|
|
self.shutting_down.emit()
|
|
|
|
@pyqtSlot(QWebEngineCertificateError)
|
|
def _handle_certificate_error(self, qt_error):
|
|
"""Handle certificate errors coming from Qt."""
|
|
error = certificateerror.CertificateErrorWrapper(qt_error)
|
|
self.certificate_error.emit(error)
|
|
# Right now, we never defer accepting, due to a PyQt bug
|
|
return error.certificate_was_accepted()
|
|
|
|
if machinery.IS_QT5:
|
|
# Overridden method instead of signal
|
|
certificateError = _handle_certificate_error # noqa: N815
|
|
|
|
def javaScriptConfirm(self, url, js_msg):
|
|
"""Override javaScriptConfirm to use qutebrowser prompts."""
|
|
if self._is_shutting_down:
|
|
return False
|
|
try:
|
|
return shared.javascript_confirm(
|
|
url, js_msg, abort_on=[self.loadStarted, self.shutting_down])
|
|
except shared.CallSuper:
|
|
return super().javaScriptConfirm(url, js_msg)
|
|
|
|
def javaScriptPrompt(self, url, js_msg, default):
|
|
"""Override javaScriptPrompt to use qutebrowser prompts."""
|
|
if self._is_shutting_down:
|
|
return (False, "")
|
|
try:
|
|
return shared.javascript_prompt(
|
|
url, js_msg, default, abort_on=[self.loadStarted, self.shutting_down])
|
|
except shared.CallSuper:
|
|
return super().javaScriptPrompt(url, js_msg, default)
|
|
|
|
def javaScriptAlert(self, url, js_msg):
|
|
"""Override javaScriptAlert to use qutebrowser prompts."""
|
|
if self._is_shutting_down:
|
|
return
|
|
try:
|
|
shared.javascript_alert(
|
|
url, js_msg, abort_on=[self.loadStarted, self.shutting_down])
|
|
except shared.CallSuper:
|
|
super().javaScriptAlert(url, js_msg)
|
|
|
|
def javaScriptConsoleMessage(self, level, msg, line, source):
|
|
"""Log javascript messages to qutebrowser's log."""
|
|
shared.javascript_log_message(self._JS_LOG_LEVEL_MAPPING[level], source, line, msg)
|
|
|
|
def acceptNavigationRequest(self,
|
|
url: QUrl,
|
|
typ: QWebEnginePage.NavigationType,
|
|
is_main_frame: bool) -> bool:
|
|
"""Override acceptNavigationRequest to forward it to the tab API."""
|
|
navigation = usertypes.NavigationRequest(
|
|
url=url,
|
|
navigation_type=self._NAVIGATION_TYPE_MAPPING.get(
|
|
typ, usertypes.NavigationRequest.Type.other),
|
|
is_main_frame=is_main_frame)
|
|
self.navigation_request.emit(navigation)
|
|
return navigation.accepted
|
|
|
|
def chooseFiles(
|
|
self,
|
|
mode: QWebEnginePage.FileSelectionMode,
|
|
old_files: Iterable[Optional[str]],
|
|
accepted_mimetypes: Iterable[Optional[str]],
|
|
) -> List[str]:
|
|
"""Override chooseFiles to (optionally) invoke custom file uploader."""
|
|
accepted_mimetypes_filtered = [m for m in accepted_mimetypes if m is not None]
|
|
old_files_filtered = [f for f in old_files if f is not None]
|
|
extra_suffixes = extra_suffixes_workaround(accepted_mimetypes_filtered)
|
|
if extra_suffixes:
|
|
log.webview.debug(
|
|
"adding extra suffixes to filepicker: "
|
|
f"before={accepted_mimetypes_filtered} "
|
|
f"added={extra_suffixes}",
|
|
)
|
|
accepted_mimetypes_filtered = list(
|
|
accepted_mimetypes_filtered
|
|
) + list(extra_suffixes)
|
|
|
|
handler = config.val.fileselect.handler
|
|
if handler == "default":
|
|
return super().chooseFiles(
|
|
mode, old_files_filtered, accepted_mimetypes_filtered,
|
|
)
|
|
assert handler == "external", handler
|
|
try:
|
|
qb_mode = _QB_FILESELECTION_MODES[mode]
|
|
except KeyError:
|
|
log.webview.warning(
|
|
f"Got file selection mode {mode}, but we don't support that!"
|
|
)
|
|
return super().chooseFiles(
|
|
mode, old_files_filtered, accepted_mimetypes_filtered,
|
|
)
|
|
|
|
return shared.choose_file(qb_mode=qb_mode)
|