1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: 2 3# Copyright 2019-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"""Handling of Qt qss stylesheets.""" 21 22import functools 23from typing import Optional, FrozenSet 24 25from PyQt5.QtCore import pyqtSlot, QObject 26 27from qutebrowser.config import config 28from qutebrowser.misc import debugcachestats 29from qutebrowser.utils import jinja, log 30 31 32def set_register(obj: QObject, 33 stylesheet: str = None, *, 34 update: bool = True) -> None: 35 """Set the stylesheet for an object. 36 37 Also, register an update when the config is changed. 38 39 Args: 40 obj: The object to set the stylesheet for and register. 41 Must have a STYLESHEET attribute if stylesheet is not given. 42 stylesheet: The stylesheet to use. 43 update: Whether to update the stylesheet on config changes. 44 """ 45 observer = _StyleSheetObserver(obj, stylesheet, update) 46 observer.register() 47 48 49@debugcachestats.register() 50@functools.lru_cache() 51def _render_stylesheet(stylesheet: str) -> str: 52 """Render the given stylesheet jinja template.""" 53 with jinja.environment.no_autoescape(): 54 template = jinja.environment.from_string(stylesheet) 55 return template.render(conf=config.val) 56 57 58def init() -> None: 59 config.instance.changed.connect(_render_stylesheet.cache_clear) 60 61 62class _StyleSheetObserver(QObject): 63 64 """Set the stylesheet on the given object and update it on changes. 65 66 Attributes: 67 _obj: The object to observe. 68 _stylesheet: The stylesheet template to use. 69 _options: The config options that the stylesheet uses. When it's not 70 necessary to listen for config changes, this attribute may be 71 None. 72 """ 73 74 def __init__(self, obj: QObject, 75 stylesheet: Optional[str], update: bool) -> None: 76 super().__init__() 77 self._obj = obj 78 self._update = update 79 80 # We only need to hang around if we are asked to update. 81 if update: 82 self.setParent(self._obj) 83 if stylesheet is None: 84 self._stylesheet: str = obj.STYLESHEET 85 else: 86 self._stylesheet = stylesheet 87 88 if update: 89 self._options: Optional[FrozenSet[str]] = jinja.template_config_variables( 90 self._stylesheet) 91 else: 92 self._options = None 93 94 def _get_stylesheet(self) -> str: 95 """Format a stylesheet based on a template. 96 97 Return: 98 The formatted template as string. 99 """ 100 return _render_stylesheet(self._stylesheet) 101 102 @pyqtSlot(str) 103 def _maybe_update_stylesheet(self, option: str) -> None: 104 """Update the stylesheet for obj if the option changed affects it.""" 105 assert self._options is not None 106 if option in self._options: 107 self._obj.setStyleSheet(self._get_stylesheet()) 108 109 def register(self) -> None: 110 """Do a first update and listen for more.""" 111 qss = self._get_stylesheet() 112 log.config.vdebug( # type: ignore[attr-defined] 113 "stylesheet for {}: {}".format(self._obj.__class__.__name__, qss)) 114 self._obj.setStyleSheet(qss) 115 if self._update: 116 config.instance.changed.connect(self._maybe_update_stylesheet) 117