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