1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: 2 3# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org> 4# 5# This file is part of qutebrowser. 6# 7# qutebrowser is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# qutebrowser is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. 19 20"""The main browser widget for QtWebEngine.""" 21 22from typing import List, Iterable 23 24from PyQt5.QtCore import pyqtSignal, QUrl 25from PyQt5.QtGui import QPalette 26from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage 27 28from qutebrowser.browser import shared 29from qutebrowser.browser.webengine import webenginesettings, certificateerror 30from qutebrowser.config import config 31from qutebrowser.utils import log, debug, usertypes 32 33 34_QB_FILESELECTION_MODES = { 35 QWebEnginePage.FileSelectOpen: shared.FileSelectionMode.single_file, 36 QWebEnginePage.FileSelectOpenMultiple: shared.FileSelectionMode.multiple_files, 37 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-91489 38 # 39 # QtWebEngine doesn't expose this value from its internal 40 # FilePickerControllerPrivate::FileChooserMode enum (i.e. it's not included in 41 # the public QWebEnginePage::FileSelectionMode enum). 42 # However, QWebEnginePage::chooseFiles is still called with the matching value 43 # (2) when a file input with "webkitdirectory" is used. 44 QWebEnginePage.FileSelectionMode(2): shared.FileSelectionMode.folder, 45} 46 47 48class WebEngineView(QWebEngineView): 49 50 """Custom QWebEngineView subclass with qutebrowser-specific features.""" 51 52 def __init__(self, *, tabdata, win_id, private, parent=None): 53 super().__init__(parent) 54 self._win_id = win_id 55 self._tabdata = tabdata 56 57 theme_color = self.style().standardPalette().color(QPalette.Base) 58 if private: 59 assert webenginesettings.private_profile is not None 60 profile = webenginesettings.private_profile 61 assert profile.isOffTheRecord() 62 else: 63 profile = webenginesettings.default_profile 64 page = WebEnginePage(theme_color=theme_color, profile=profile, 65 parent=self) 66 self.setPage(page) 67 68 def render_widget(self): 69 """Get the RenderWidgetHostViewQt for this view.""" 70 return self.focusProxy() 71 72 def shutdown(self): 73 self.page().shutdown() 74 75 def createWindow(self, wintype): 76 """Called by Qt when a page wants to create a new window. 77 78 This function is called from the createWindow() method of the 79 associated QWebEnginePage, each time the page wants to create a new 80 window of the given type. This might be the result, for example, of a 81 JavaScript request to open a document in a new window. 82 83 Args: 84 wintype: This enum describes the types of window that can be 85 created by the createWindow() function. 86 87 QWebEnginePage::WebBrowserWindow: 88 A complete web browser window. 89 QWebEnginePage::WebBrowserTab: 90 A web browser tab. 91 QWebEnginePage::WebDialog: 92 A window without decoration. 93 QWebEnginePage::WebBrowserBackgroundTab: 94 A web browser tab without hiding the current visible 95 WebEngineView. 96 97 Return: 98 The new QWebEngineView object. 99 """ 100 debug_type = debug.qenum_key(QWebEnginePage, wintype) 101 background = config.val.tabs.background 102 103 log.webview.debug("createWindow with type {}, background {}".format( 104 debug_type, background)) 105 106 if wintype == QWebEnginePage.WebBrowserWindow: 107 # Shift-Alt-Click 108 target = usertypes.ClickTarget.window 109 elif wintype == QWebEnginePage.WebDialog: 110 log.webview.warning("{} requested, but we don't support " 111 "that!".format(debug_type)) 112 target = usertypes.ClickTarget.tab 113 elif wintype == QWebEnginePage.WebBrowserTab: 114 # Middle-click / Ctrl-Click with Shift 115 # FIXME:qtwebengine this also affects target=_blank links... 116 if background: 117 target = usertypes.ClickTarget.tab 118 else: 119 target = usertypes.ClickTarget.tab_bg 120 elif wintype == QWebEnginePage.WebBrowserBackgroundTab: 121 # Middle-click / Ctrl-Click 122 if background: 123 target = usertypes.ClickTarget.tab_bg 124 else: 125 target = usertypes.ClickTarget.tab 126 else: 127 raise ValueError("Invalid wintype {}".format(debug_type)) 128 129 tab = shared.get_tab(self._win_id, target) 130 return tab._widget # pylint: disable=protected-access 131 132 def contextMenuEvent(self, ev): 133 """Prevent context menus when rocker gestures are enabled.""" 134 if config.val.input.mouse.rocker_gestures: 135 ev.ignore() 136 return 137 super().contextMenuEvent(ev) 138 139 140class WebEnginePage(QWebEnginePage): 141 142 """Custom QWebEnginePage subclass with qutebrowser-specific features. 143 144 Attributes: 145 _is_shutting_down: Whether the page is currently shutting down. 146 _theme_color: The theme background color. 147 148 Signals: 149 certificate_error: Emitted on certificate errors. 150 Needs to be directly connected to a slot setting the 151 'ignore' attribute. 152 shutting_down: Emitted when the page is shutting down. 153 navigation_request: Emitted on acceptNavigationRequest. 154 """ 155 156 certificate_error = pyqtSignal(certificateerror.CertificateErrorWrapper) 157 shutting_down = pyqtSignal() 158 navigation_request = pyqtSignal(usertypes.NavigationRequest) 159 160 def __init__(self, *, theme_color, profile, parent=None): 161 super().__init__(profile, parent) 162 self._is_shutting_down = False 163 self._theme_color = theme_color 164 self._set_bg_color() 165 config.instance.changed.connect(self._set_bg_color) 166 167 @config.change_filter('colors.webpage.bg') 168 def _set_bg_color(self): 169 col = config.val.colors.webpage.bg 170 if col is None: 171 col = self._theme_color 172 self.setBackgroundColor(col) 173 174 def shutdown(self): 175 self._is_shutting_down = True 176 self.shutting_down.emit() 177 178 def certificateError(self, error): 179 """Handle certificate errors coming from Qt.""" 180 error = certificateerror.CertificateErrorWrapper(error) 181 self.certificate_error.emit(error) 182 return error.ignore 183 184 def javaScriptConfirm(self, url, js_msg): 185 """Override javaScriptConfirm to use qutebrowser prompts.""" 186 if self._is_shutting_down: 187 return False 188 try: 189 return shared.javascript_confirm( 190 url, js_msg, abort_on=[self.loadStarted, self.shutting_down]) 191 except shared.CallSuper: 192 return super().javaScriptConfirm(url, js_msg) 193 194 def javaScriptPrompt(self, url, js_msg, default): 195 """Override javaScriptPrompt to use qutebrowser prompts.""" 196 if self._is_shutting_down: 197 return (False, "") 198 try: 199 return shared.javascript_prompt( 200 url, js_msg, default, abort_on=[self.loadStarted, self.shutting_down]) 201 except shared.CallSuper: 202 return super().javaScriptPrompt(url, js_msg, default) 203 204 def javaScriptAlert(self, url, js_msg): 205 """Override javaScriptAlert to use qutebrowser prompts.""" 206 if self._is_shutting_down: 207 return 208 try: 209 shared.javascript_alert( 210 url, js_msg, abort_on=[self.loadStarted, self.shutting_down]) 211 except shared.CallSuper: 212 super().javaScriptAlert(url, js_msg) 213 214 def javaScriptConsoleMessage(self, level, msg, line, source): 215 """Log javascript messages to qutebrowser's log.""" 216 level_map = { 217 QWebEnginePage.InfoMessageLevel: usertypes.JsLogLevel.info, 218 QWebEnginePage.WarningMessageLevel: usertypes.JsLogLevel.warning, 219 QWebEnginePage.ErrorMessageLevel: usertypes.JsLogLevel.error, 220 } 221 shared.javascript_log_message(level_map[level], source, line, msg) 222 223 def acceptNavigationRequest(self, 224 url: QUrl, 225 typ: QWebEnginePage.NavigationType, 226 is_main_frame: bool) -> bool: 227 """Override acceptNavigationRequest to forward it to the tab API.""" 228 type_map = { 229 QWebEnginePage.NavigationTypeLinkClicked: 230 usertypes.NavigationRequest.Type.link_clicked, 231 QWebEnginePage.NavigationTypeTyped: 232 usertypes.NavigationRequest.Type.typed, 233 QWebEnginePage.NavigationTypeFormSubmitted: 234 usertypes.NavigationRequest.Type.form_submitted, 235 QWebEnginePage.NavigationTypeBackForward: 236 usertypes.NavigationRequest.Type.back_forward, 237 QWebEnginePage.NavigationTypeReload: 238 usertypes.NavigationRequest.Type.reloaded, 239 QWebEnginePage.NavigationTypeOther: 240 usertypes.NavigationRequest.Type.other, 241 } 242 try: 243 type_map[QWebEnginePage.NavigationTypeRedirect] = ( 244 usertypes.NavigationRequest.Type.redirect) 245 except AttributeError: 246 # Added in Qt 5.14 247 pass 248 249 navigation = usertypes.NavigationRequest( 250 url=url, 251 navigation_type=type_map.get( 252 typ, usertypes.NavigationRequest.Type.other), 253 is_main_frame=is_main_frame) 254 self.navigation_request.emit(navigation) 255 return navigation.accepted 256 257 def chooseFiles( 258 self, 259 mode: QWebEnginePage.FileSelectionMode, 260 old_files: Iterable[str], 261 accepted_mimetypes: Iterable[str], 262 ) -> List[str]: 263 """Override chooseFiles to (optionally) invoke custom file uploader.""" 264 handler = config.val.fileselect.handler 265 if handler == "default": 266 return super().chooseFiles(mode, old_files, accepted_mimetypes) 267 assert handler == "external", handler 268 try: 269 qb_mode = _QB_FILESELECTION_MODES[mode] 270 except KeyError: 271 log.webview.warning( 272 f"Got file selection mode {mode}, but we don't support that!" 273 ) 274 return super().chooseFiles(mode, old_files, accepted_mimetypes) 275 276 return shared.choose_file(qb_mode=qb_mode) 277