summaryrefslogtreecommitdiff
path: root/qutebrowser/config/stylesheet.py
blob: 2c0c5fb075845054f578ee1e672b6e71039844f7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:

# Copyright 2019-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser.  If not, see <https://www.gnu.org/licenses/>.

"""Handling of Qt qss stylesheets."""

import functools
from typing import Optional, FrozenSet

from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtWidgets import QWidget

from qutebrowser.config import config
from qutebrowser.misc import debugcachestats
from qutebrowser.utils import jinja, log


def set_register(obj: QObject,
                 stylesheet: str = None, *,
                 update: bool = True) -> None:
    """Set the stylesheet for an object.

    Also, register an update when the config is changed.

    Args:
        obj: The object to set the stylesheet for and register.
             Must have a STYLESHEET attribute if stylesheet is not given.
        stylesheet: The stylesheet to use.
        update: Whether to update the stylesheet on config changes.
    """
    observer = _StyleSheetObserver(obj, stylesheet, update)
    observer.register()


@debugcachestats.register()
@functools.lru_cache()
def _render_stylesheet(stylesheet: str) -> str:
    """Render the given stylesheet jinja template."""
    with jinja.environment.no_autoescape():
        template = jinja.environment.from_string(stylesheet)
    return template.render(conf=config.val)


def init() -> None:
    config.instance.changed.connect(_render_stylesheet.cache_clear)


class _StyleSheetObserver(QObject):

    """Set the stylesheet on the given object and update it on changes.

    Attributes:
        _obj: The object to observe.
        _stylesheet: The stylesheet template to use.
        _options: The config options that the stylesheet uses. When it's not
                  necessary to listen for config changes, this attribute may be
                  None.
    """

    def __init__(self, obj: QWidget,
                 stylesheet: Optional[str], update: bool) -> None:
        super().__init__()
        self._obj = obj
        self._update = update

        # We only need to hang around if we are asked to update.
        if update:
            self.setParent(self._obj)
        if stylesheet is None:
            self._stylesheet: str = obj.STYLESHEET
        else:
            self._stylesheet = stylesheet

        if update:
            self._options: Optional[FrozenSet[str]] = jinja.template_config_variables(
                self._stylesheet)
        else:
            self._options = None

    def _get_stylesheet(self) -> str:
        """Format a stylesheet based on a template.

        Return:
            The formatted template as string.
        """
        return _render_stylesheet(self._stylesheet)

    @pyqtSlot(str)
    def _maybe_update_stylesheet(self, option: str) -> None:
        """Update the stylesheet for obj if the option changed affects it."""
        assert self._options is not None
        if option in self._options:
            self._obj.setStyleSheet(self._get_stylesheet())

    def register(self) -> None:
        """Do a first update and listen for more."""
        qss = self._get_stylesheet()
        log.config.vdebug(  # type: ignore[attr-defined]
            "stylesheet for {}: {}".format(self._obj.__class__.__name__, qss))
        self._obj.setStyleSheet(qss)
        if self._update:
            config.instance.changed.connect(self._maybe_update_stylesheet)