diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 2302a0df4..417465929 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -26,16 +26,12 @@ Module attributes: import os -import sip from PyQt5.QtGui import QFont -from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile, - QWebEngineScript) +from PyQt5.QtWebEngineWidgets import QWebEngineSettings, QWebEngineProfile -from qutebrowser.browser import shared from qutebrowser.browser.webengine import spell from qutebrowser.config import config, websettings -from qutebrowser.utils import (utils, standarddir, javascript, qtutils, - message, log, objreg) +from qutebrowser.utils import utils, standarddir, qtutils, message, log # The default QWebEngineProfile default_profile = None @@ -178,8 +174,6 @@ class ProfileSetter: def init_profile(self): """Initialize settings on the given profile.""" - self._init_js() - self.init_stylesheet() self.set_http_headers() self.set_http_cache_size() self._profile.settings().setAttribute( @@ -188,64 +182,6 @@ class ProfileSetter: self._profile.setSpellCheckEnabled(True) self.set_dictionary_language() - def _inject_early_js(self, name, js_code, *, - world=QWebEngineScript.ApplicationWorld, - subframes=False): - """Inject the given script to run early on a page load. - - This runs the script both on DocumentCreation and DocumentReady as on - some internal pages, DocumentCreation will not work. - - That is a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66011 - """ - for injection in ['creation', 'ready']: - injection_points = { - 'creation': QWebEngineScript.DocumentCreation, - 'ready': QWebEngineScript.DocumentReady, - } - script = QWebEngineScript() - script.setInjectionPoint(injection_points[injection]) - script.setSourceCode(js_code) - script.setWorldId(world) - script.setRunsOnSubFrames(subframes) - script.setName('_qute_{}_{}'.format(name, injection)) - self._profile.scripts().insert(script) - - def _remove_early_js(self, name): - """Remove an early QWebEngineScript.""" - scripts = self._profile.scripts() - for injection in ['creation', 'ready']: - full_name = '_qute_{}_{}'.format(name, injection) - script = scripts.findScript(full_name) - if not script.isNull(): - scripts.remove(script) - - def _init_js(self): - """Initialize global qutebrowser JavaScript.""" - js_code = javascript.wrap_global( - 'scripts', - utils.read_file('javascript/scroll.js'), - utils.read_file('javascript/webelem.js'), - utils.read_file('javascript/caret.js'), - ) - # FIXME:qtwebengine what about subframes=True? - self._inject_early_js('js', js_code, subframes=True) - - def init_stylesheet(self): - """Initialize custom stylesheets. - - Partially inspired by QupZilla: - https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/app/mainapplication.cpp#L1063-L1101 - """ - self._remove_early_js('stylesheet') - css = shared.get_user_stylesheet() - js_code = javascript.wrap_global( - 'stylesheet', - utils.read_file('javascript/stylesheet.js'), - javascript.assemble('stylesheet', 'set_css', css), - ) - self._inject_early_js('stylesheet', js_code, subframes=True) - def set_http_headers(self): """Set the user agent and accept-language for the given profile. @@ -296,30 +232,12 @@ class ProfileSetter: self._profile.setSpellCheckLanguages(filenames) -def _update_stylesheet(): - """Update the custom stylesheet in existing tabs.""" - css = shared.get_user_stylesheet() - code = javascript.assemble('stylesheet', 'set_css', css) - for win_id, window in objreg.window_registry.items(): - # We could be in the middle of destroying a window here - if sip.isdeleted(window): - continue - tab_registry = objreg.get('tab-registry', scope='window', - window=win_id) - for tab in tab_registry.values(): - tab.run_js_async(code) - - def _update_settings(option): """Update global settings when qwebsettings changed.""" global_settings.update_setting(option) - if option in ['scrolling.bar', 'content.user_stylesheets']: - default_profile.setter.init_stylesheet() - private_profile.setter.init_stylesheet() - _update_stylesheet() - elif option in ['content.headers.user_agent', - 'content.headers.accept_language']: + if option in ['content.headers.user_agent', + 'content.headers.accept_language']: default_profile.setter.set_http_headers() private_profile.setter.set_http_headers() elif option == 'content.cache.size': @@ -354,43 +272,6 @@ def _init_profiles(): private_profile.setter.init_profile() -def inject_userscripts(): - """Register user JavaScript files with the global profiles.""" - # The Greasemonkey metadata block support in QtWebEngine only starts at - # Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in response - # to urlChanged. - if not qtutils.version_check('5.8'): - return - - # Since we are inserting scripts into profile.scripts they won't - # just get replaced by new gm scripts like if we were injecting them - # ourselves so we need to remove all gm scripts, while not removing - # any other stuff that might have been added. Like the one for - # stylesheets. - greasemonkey = objreg.get('greasemonkey') - for profile in [default_profile, private_profile]: - scripts = profile.scripts() - for script in scripts.toList(): - if script.name().startswith("GM-"): - log.greasemonkey.debug('Removing script: {}' - .format(script.name())) - removed = scripts.remove(script) - assert removed, script.name() - - # Then add the new scripts. - for script in greasemonkey.all_scripts(): - # @run-at (and @include/@exclude/@match) is parsed by - # QWebEngineScript. - new_script = QWebEngineScript() - new_script.setWorldId(QWebEngineScript.MainWorld) - new_script.setSourceCode(script.code()) - new_script.setName("GM-{}".format(script.name)) - new_script.setRunsOnSubFrames(script.runs_on_sub_frames) - log.greasemonkey.debug('adding script: {}' - .format(new_script.name())) - scripts.insert(new_script) - - def init(args): """Initialize the global QWebSettings.""" if args.enable_webengine_inspector: diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index f9308163d..5d7dfeb90 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -33,7 +33,7 @@ from PyQt5.QtNetwork import QAuthenticator from PyQt5.QtWidgets import QApplication from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript -from qutebrowser.config import configdata +from qutebrowser.config import configdata, config from qutebrowser.browser import browsertab, mouse, shared from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory, interceptor, webenginequtescheme, @@ -73,10 +73,6 @@ def init(): download_manager.install(webenginesettings.private_profile) objreg.register('webengine-download-manager', download_manager) - greasemonkey = objreg.get('greasemonkey') - greasemonkey.scripts_reloaded.connect(webenginesettings.inject_userscripts) - webenginesettings.inject_userscripts() - # Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs. _JS_WORLD_MAP = { @@ -633,6 +629,119 @@ class WebEngineTab(browsertab.AbstractTab): self._child_event_filter = None self._saved_zoom = None self._reload_url = None + config.instance.changed.connect(self._on_config_changed) + self._init_js() + + @pyqtSlot(str) + def _on_config_changed(self, option): + if option in ['scrolling.bar', 'content.user_stylesheets']: + self._init_stylesheet() + self._update_stylesheet() + + def _update_stylesheet(self): + """Update the custom stylesheet in existing tabs.""" + css = shared.get_user_stylesheet() + code = javascript.assemble('stylesheet', 'set_css', css) + self.run_js_async(code) + + def _inject_early_js(self, name, js_code, *, + world=QWebEngineScript.ApplicationWorld, + subframes=False): + """Inject the given script to run early on a page load. + + This runs the script both on DocumentCreation and DocumentReady as on + some internal pages, DocumentCreation will not work. + + That is a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66011 + """ + scripts = self._widget.page().scripts() + for injection in ['creation', 'ready']: + injection_points = { + 'creation': QWebEngineScript.DocumentCreation, + 'ready': QWebEngineScript.DocumentReady, + } + script = QWebEngineScript() + script.setInjectionPoint(injection_points[injection]) + script.setSourceCode(js_code) + script.setWorldId(world) + script.setRunsOnSubFrames(subframes) + script.setName('_qute_{}_{}'.format(name, injection)) + scripts.insert(script) + + def _remove_early_js(self, name): + """Remove an early QWebEngineScript.""" + scripts = self._widget.page().scripts() + for injection in ['creation', 'ready']: + full_name = '_qute_{}_{}'.format(name, injection) + script = scripts.findScript(full_name) + if not script.isNull(): + scripts.remove(script) + + def _init_js(self): + """Initialize global qutebrowser JavaScript.""" + js_code = javascript.wrap_global( + 'scripts', + utils.read_file('javascript/scroll.js'), + utils.read_file('javascript/webelem.js'), + utils.read_file('javascript/caret.js'), + ) + # FIXME:qtwebengine what about subframes=True? + self._inject_early_js('js', js_code, subframes=True) + self._init_stylesheet() + + greasemonkey = objreg.get('greasemonkey') + greasemonkey.scripts_reloaded.connect(self._inject_userscripts) + self._inject_userscripts() + + def _init_stylesheet(self): + """Initialize custom stylesheets. + + Partially inspired by QupZilla: + https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/app/mainapplication.cpp#L1063-L1101 + """ + self._remove_early_js('stylesheet') + css = shared.get_user_stylesheet() + js_code = javascript.wrap_global( + 'stylesheet', + utils.read_file('javascript/stylesheet.js'), + javascript.assemble('stylesheet', 'set_css', css), + ) + self._inject_early_js('stylesheet', js_code, subframes=True) + + def _inject_userscripts(self): + """Register user JavaScript files with the global profiles.""" + # The Greasemonkey metadata block support in QtWebEngine only starts at + # Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in + # response to urlChanged. + if not qtutils.version_check('5.8'): + return + + # Since we are inserting scripts into profile.scripts they won't + # just get replaced by new gm scripts like if we were injecting them + # ourselves so we need to remove all gm scripts, while not removing + # any other stuff that might have been added. Like the one for + # stylesheets. + greasemonkey = objreg.get('greasemonkey') + scripts = self._widget.page().scripts() + for script in scripts.toList(): + if script.name().startswith("GM-"): + log.greasemonkey.debug('Removing script: {}' + .format(script.name())) + removed = scripts.remove(script) + assert removed, script.name() + + # Then add the new scripts. + for script in greasemonkey.all_scripts(): + # @run-at (and @include/@exclude/@match) is parsed by + # QWebEngineScript. + new_script = QWebEngineScript() + new_script.setWorldId(QWebEngineScript.MainWorld) + new_script.setSourceCode(script.code()) + new_script.setName("GM-{}".format(script.name)) + new_script.setRunsOnSubFrames(script.runs_on_sub_frames) + log.greasemonkey.debug('adding script: {}' + .format(new_script.name())) + scripts.insert(new_script) def _install_event_filter(self): self._widget.focusProxy().installEventFilter(self._mouse_event_filter) diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature index 637f4696c..fb48a5932 100644 --- a/tests/end2end/features/javascript.feature +++ b/tests/end2end/features/javascript.feature @@ -174,3 +174,15 @@ Feature: Javascript stuff When I set content.javascript.enabled to false And I open 500 without waiting Then "Showing error page for* 500" should be logged + + Scenario: Using JS after window.open + When I open data/hello.txt + And I set content.javascript.can_open_tabs_automatically to true + And I run :jseval window.open('about:blank') + And I open data/hello.txt + And I run :tab-only + And I open data/hints/html/simple.html + And I run :hint all + And I wait for "hints: a" in the log + And I run :leave-mode + Then "There was an error while getting hint elements" should not be logged