qutebrowser/stylesheet.patch

518 lines
20 KiB
Diff

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;
})();