diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 194dc5b36..7ff0b1a89 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -25,7 +25,7 @@ import netrc from PyQt5.QtCore import QUrl -from qutebrowser.config import config +from qutebrowser.config import config, configutils from qutebrowser.utils import usertypes, message, log, objreg, jinja, utils from qutebrowser.mainwindow import mainwindow @@ -273,20 +273,31 @@ def get_tab(win_id, target): return tabbed_browser.tabopen(url=None, background=bg_tab) -def get_user_stylesheet(searching=False): - """Get the combined user-stylesheet.""" +def _wrap_bar(css: str, searching: bool): + """Wrap the passed css in a bar if needed, depending on settings.""" + if css is not configutils.UNSET and \ + (config.val.scrolling.bar == 'never' or + config.val.scrolling.bar == 'when-searching' and not searching): + css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }' + return css + +def get_user_stylesheet(searching=False, url=None): + """Get the combined user-stylesheet. + + If `url` is given and there's no overridden stylesheet, return + `configutils.UNSET`. + """ css = '' - stylesheets = config.val.content.user_stylesheets + stylesheets = config.instance.get('content.user_stylesheets', url, + fallback=url is None) + if stylesheets is configutils.UNSET: + return _wrap_bar(stylesheets, searching) for filename in stylesheets: with open(filename, 'r', encoding='utf-8') as f: css += f.read() - if (config.val.scrolling.bar == 'never' or - config.val.scrolling.bar == 'when-searching' and not searching): - css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }' - - return css + return _wrap_bar(css, searching) def netrc_authentication(url, authenticator): diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 9ecdb1955..2fee55568 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -31,7 +31,7 @@ from PyQt5.QtNetwork import QAuthenticator from PyQt5.QtWidgets import QApplication from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript -from qutebrowser.config import configdata, config +from qutebrowser.config import configdata, config, configutils from qutebrowser.browser import browsertab, mouse, shared, webelem from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory, interceptor, webenginequtescheme, @@ -863,23 +863,43 @@ class _WebEngineScripts(QObject): def connect_signals(self): """Connect signals to our private slots.""" config.instance.changed.connect(self._on_config_changed) + self._tab.url_changed.connect(self._update_stylesheet) + self._tab.load_finished.connect(self._on_load_finished) - self._tab.search.cleared.connect(functools.partial( - self._update_stylesheet, searching=False)) - self._tab.search.finished.connect(self._update_stylesheet) + self._tab.search.cleared.connect( + lambda: self._update_stylesheet(self._tab.url(), searching=False, force=True)) + self._tab.search.finished.connect( + lambda found: self._update_stylesheet(self._tab.url(), searching=found, force=True)) @pyqtSlot(str) def _on_config_changed(self, option): if option in ['scrolling.bar', 'content.user_stylesheets']: self._init_stylesheet() - self._update_stylesheet() + self._update_stylesheet(self._tab.url(), force=True) - @pyqtSlot(bool) - def _update_stylesheet(self, searching=False): - """Update the custom stylesheet in existing tabs.""" - css = shared.get_user_stylesheet(searching=searching) - code = javascript.assemble('stylesheet', 'set_css', css) - self._tab.run_js_async(code) + @pyqtSlot() + def _on_load_finished(self, searching=False): + url = self._tab.url() + self._update_stylesheet(url, searching=searching) + + @pyqtSlot(QUrl) + def _update_stylesheet(self, url, searching=False, force=False): + """Update the custom stylesheet in existing tabs. + + Arguments: + url: The url to get the stylesheet for. + force: Also update the global stylesheet. + """ + if not url.isValid(): + # FIXME should we be dropping this request completely? + url = None + css = shared.get_user_stylesheet(searching=searching, url=url) + if css is configutils.UNSET and force: + css = shared.get_user_stylesheet(searching=searching, url=None) + + if css is not configutils.UNSET: + code = javascript.assemble('stylesheet', 'set_css', css) + self._tab.run_js_async(code) def _inject_early_js(self, name, js_code, *, world=QWebEngineScript.ApplicationWorld, @@ -952,11 +972,11 @@ class _WebEngineScripts(QObject): 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() + css = shared.get_user_stylesheet(url=None) js_code = javascript.wrap_global( 'stylesheet', utils.read_file('javascript/stylesheet.js'), - javascript.assemble('stylesheet', 'set_css', css), + javascript.assemble('stylesheet', 'set_new_page_css', css), ) self._inject_early_js('stylesheet', js_code, subframes=True) diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index fac86285e..b7a92a6d4 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -29,7 +29,7 @@ import os.path from PyQt5.QtGui import QFont from PyQt5.QtWebKit import QWebSettings -from qutebrowser.config import config, websettings +from qutebrowser.config import config, websettings, configutils from qutebrowser.config.websettings import AttributeInfo as Attr from qutebrowser.utils import standarddir, urlutils from qutebrowser.browser import shared @@ -120,43 +120,50 @@ class WebKitSettings(websettings.AbstractSettings): QWebSettings.FantasyFont: QFont.Fantasy, } - -def _set_user_stylesheet(settings): - """Set the generated user-stylesheet.""" - stylesheet = shared.get_user_stylesheet().encode('utf-8') - url = urlutils.data_url('text/css;charset=utf-8', stylesheet) - settings.setUserStyleSheetUrl(url) - - -def _set_cookie_accept_policy(settings): - """Update the content.cookies.accept setting.""" - mapping = { - 'all': QWebSettings.AlwaysAllowThirdPartyCookies, - 'no-3rdparty': QWebSettings.AlwaysBlockThirdPartyCookies, - 'never': QWebSettings.AlwaysBlockThirdPartyCookies, - 'no-unknown-3rdparty': QWebSettings.AllowThirdPartyWithExistingCookies, - } - value = config.val.content.cookies.accept - settings.setThirdPartyCookiePolicy(mapping[value]) - - -def _set_cache_maximum_pages(settings): - """Update the content.cache.maximum_pages setting.""" - value = config.val.content.cache.maximum_pages - settings.setMaximumPagesInCache(value) - - -def _update_settings(option): - """Update global settings when qwebsettings changed.""" - global_settings.update_setting(option) - - settings = QWebSettings.globalSettings() - if option in ['scrollbar.hide', 'content.user_stylesheets']: - _set_user_stylesheet(settings) - elif option == 'content.cookies.accept': - _set_cookie_accept_policy(settings) - elif option == 'content.cache.maximum_pages': - _set_cache_maximum_pages(settings) + def _set_user_stylesheet(self, url=None): + """Set the generated user-stylesheet.""" + stylesheet = shared.get_user_stylesheet(url=url) + if stylesheet is configutils.UNSET: + return + url = urlutils.data_url('text/css;charset=utf-8', + stylesheet.encode('utf-8')) + self._settings.setUserStyleSheetUrl(url) + + def _set_cookie_accept_policy(self): + """Update the content.cookies.accept setting.""" + mapping = { + 'all': QWebSettings.AlwaysAllowThirdPartyCookies, + 'no-3rdparty': QWebSettings.AlwaysBlockThirdPartyCookies, + 'never': QWebSettings.AlwaysBlockThirdPartyCookies, + 'no-unknown-3rdparty': QWebSettings.AllowThirdPartyWithExistingCookies, + } + value = config.val.content.cookies.accept + self._settings.setThirdPartyCookiePolicy(mapping[value]) + + def _set_cache_maximum_pages(self): + """Update the content.cache.maximum_pages setting.""" + value = config.val.content.cache.maximum_pages + self._settings.setMaximumPagesInCache(value) + + def update_setting(self, option): + if option in ['scrollbar.hide', 'content.user_stylesheets']: + self._set_user_stylesheet() + elif option == 'content.cookies.accept': + self._set_cookie_accept_policy() + elif option == 'content.cache.maximum_pages': + self._set_cache_maximum_pages() + else: + super().update_setting(option) + + def update_for_url(self, url): + super().update_for_url(url) + self._set_user_stylesheet(url) + + def init_settings(self): + super().init_settings() + self._set_user_stylesheet() + self._set_cookie_accept_policy() + self._set_cache_maximum_pages() def init(_args): @@ -172,16 +179,10 @@ def init(_args): QWebSettings.setOfflineStoragePath( os.path.join(data_path, 'offline-storage')) - settings = QWebSettings.globalSettings() - _set_user_stylesheet(settings) - _set_cookie_accept_policy(settings) - _set_cache_maximum_pages(settings) - - config.instance.changed.connect(_update_settings) - global global_settings global_settings = WebKitSettings(QWebSettings.globalSettings()) global_settings.init_settings() + config.instance.changed.connect(global_settings.update_setting) def shutdown(): diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index a51126c1b..f2126f7e3 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -353,10 +353,15 @@ class Config(QObject): """Get the given setting converted for Python code. Args: - fallback: Use the global value if there's no URL-specific one. + name: The name of the setting to get. + url: The QUrl to get the setting for. + fallback: If False, return configutils.UNSET when there's no + override for this domain. """ opt = self.get_opt(name) obj = self.get_obj(name, url=url, fallback=fallback) + if obj is configutils.UNSET: + return obj return opt.typ.to_py(obj) def _maybe_copy(self, value: Any) -> Any: @@ -378,6 +383,12 @@ class Config(QObject): Note that the returned values are not watched for mutation. If a URL is given, return the value which should be used for that URL. + + Args: + name: The name of the setting to get. + url: The QUrl to get the setting for. + fallback: If False, return configutils.UNSET when there's no + override for this domain. """ self.get_opt(name) # To make sure it exists value = self._values[name].get_for_url(url, fallback=fallback) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index e2d443280..bafa250fb 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -742,6 +742,7 @@ content.user_stylesheets: valtype: File none_ok: True default: [] + supports_pattern: true desc: List of user stylesheet filenames to use. content.webgl: diff --git a/qutebrowser/javascript/stylesheet.js b/qutebrowser/javascript/stylesheet.js index 549d9b88b..4e144097f 100644 --- a/qutebrowser/javascript/stylesheet.js +++ b/qutebrowser/javascript/stylesheet.js @@ -31,42 +31,98 @@ window._qutebrowser.stylesheet = (function() { const svg_ns = "http://www.w3.org/2000/svg"; let root_elem; + let root_key; let style_elem; let css_content = ""; - let root_observer; - let initialized = false; + function is_stylesheet_applied(css) { + return style_elem && style_elem.textContent === css; + } - // Watch for rewrites of the root element and changes to its children, - // then move the stylesheet to the end. Partially inspired by Stylus: - // https://github.com/openstyles/stylus/blob/1.1.4.2/content/apply.js#L235-L355 - function watch_root() { + function ensure_stylesheet_loaded() { if (!document.documentElement) { - root_observer.observe(document, {"childList": true}); - return; + throw new Error( + "ensure_stylesheet_loaded called before DOM was available" + ); } - if (root_elem !== document.documentElement) { - root_elem = document.documentElement; - root_observer.disconnect(); - root_observer.observe(document, {"childList": true}); - root_observer.observe(root_elem, {"childList": true}); + if (style_elem) { + style_elem.textContent = css_content; + } else { + style_elem = create_style(css_content); } + if (style_elem !== root_elem.lastChild) { root_elem.appendChild(style_elem); } } + function ensure_root_present() { + let waiting_interval; + function is_root_present() { + return document && document.documentElement; + } + return new Promise((resolve) => { + if (is_root_present()) { + root_elem = document.documentElement; + resolve(document.documentElement); + } else { + waiting_interval = setInterval(() => { + if (!is_root_present()) { + return; + } + clearInterval(waiting_interval); + root_elem = document.documentElement; + resolve(document.documentElement); + }, 100); + } + }); + } + + function wait_for_new_root() { + let waiting_interval; + function is_new_root() { + return ( + document.documentElement && + document.documentElement.getAttribute("__qb_key") !== + root_key && + check_style(document.documentElement) + ); + } + function setup_new_root(new_root_elem) { + root_elem = new_root_elem; + root_key = new Date().getTime(); + root_elem.setAttribute("__qb_key", root_key); + // style_elem would refer to a node in the old page's dom + style_elem = null; + return root_elem; + } + return new Promise((resolve) => { + if (is_new_root()) { + resolve(setup_new_root(document.documentElement)); + } else { + waiting_interval = setInterval(() => { + if (!is_new_root()) { + return; + } + clearInterval(waiting_interval); + resolve(setup_new_root(document.documentElement)); + }, 100); + } + }); + } + function create_style() { let ns = xhtml_ns; - if (document.documentElement && - document.documentElement.namespaceURI === svg_ns) { + if ( + document.documentElement && + document.documentElement.namespaceURI === svg_ns + ) { ns = svg_ns; } style_elem = document.createElementNS(ns, "style"); style_elem.textContent = css_content; - root_observer = new MutationObserver(watch_root); - watch_root(); + return style_elem; } // We should only inject the stylesheet if the document already has style @@ -75,69 +131,44 @@ window._qutebrowser.stylesheet = (function() { // starting point for exploring the relevant code in Chromium, see // https://github.com/qt/qtwebengine-chromium/blob/cfe8c60/chromium/third_party/WebKit/Source/core/xml/parser/XMLDocumentParser.cpp#L1539-L1540 function check_style(node) { - const stylesheet = node.nodeType === Node.PROCESSING_INSTRUCTION_NODE && - node.target === "xml-stylesheet" && - node.parentNode === document; - const known_ns = node.nodeType === Node.ELEMENT_NODE && - (node.namespaceURI === xhtml_ns || - node.namespaceURI === svg_ns); + const stylesheet = + node.nodeType === Node.PROCESSING_INSTRUCTION_NODE && + node.target === "xml-stylesheet" && + node.parentNode === document; + const known_ns = + node.nodeType === Node.ELEMENT_NODE && + (node.namespaceURI === xhtml_ns || node.namespaceURI === svg_ns); return stylesheet || known_ns; } - function init() { - initialized = true; - // Chromium will not rewrite a document inside a frame, so add the - // stylesheet even if the document is unstyled. - if (window !== window.top) { - create_style(); - return; - } - const iter = document.createNodeIterator(document, - NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_ELEMENT); - let node; - while ((node = iter.nextNode())) { - if (check_style(node)) { - create_style(); + function set_css(css) { + ensure_root_present().then(() => { + if (is_stylesheet_applied(css)) { return; } - } - const style_observer = new MutationObserver((mutations) => { - for (const mutation of mutations) { - const nodes = mutation.addedNodes; - for (let i = 0; i < nodes.length; ++i) { - if (check_style(nodes[i])) { - create_style(); - style_observer.disconnect(); - return; - } + css_content = css; + + ensure_stylesheet_loaded(); + // Propagate the new CSS to all child frames. + // FIXME:qtwebengine This does not work for cross-origin frames. + for (let i = 0; i < window.frames.length; ++i) { + const frame = window.frames[i]; + if (frame._qutebrowser && frame._qutebrowser.stylesheet) { + frame._qutebrowser.stylesheet.set_css(css); } } }); - style_observer.observe(document, {"childList": true, "subtree": true}); } - funcs.set_css = function(css) { - if (!initialized) { - init(); - } - if (style_elem) { - style_elem.textContent = css; - // The browser seems to rewrite the document in same-origin frames - // without notifying the mutation observer. Ensure that the - // stylesheet is in the current document. - watch_root(); - } else { - css_content = css; - } - // Propagate the new CSS to all child frames. - // FIXME:qtwebengine This does not work for cross-origin frames. - for (let i = 0; i < window.frames.length; ++i) { - const frame = window.frames[i]; - if (frame._qutebrowser && frame._qutebrowser.stylesheet) { - frame._qutebrowser.stylesheet.set_css(css); - } - } - }; + function set_new_page_css(css) { + wait_for_new_root().then(() => { + set_css(css); + }); + } + + // exports + funcs.set_css = set_css; + funcs.set_new_page_css = set_new_page_css; return funcs; })();