diff --git a/stylesheet.patch b/stylesheet.patch new file mode 100644 index 000000000..9465125ce --- /dev/null +++ b/stylesheet.patch @@ -0,0 +1,517 @@ +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; + })();