File: uibuilder.py

package info (click to toggle)
syncthing-gtk 0.9.4.4%2Bds%2Bgit20221205%2B12a9702d29ab-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 2,888 kB
  • sloc: python: 8,077; sh: 259; xml: 134; makefile: 6
file content (225 lines) | stat: -rw-r--r-- 8,255 bytes parent folder | download | duplicates (2)
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
#!/usr/bin/env python3
"""
Syncthing-GTK - tools

Wrapper around GTKBuilder. Allows using conditional (<IF>) tags in
ui files.

Usage:
    - Crete instance
    - Enable conditions (enable_condition call)
    - Call add_from_file or add_from_string method
    - Continue as usual
"""


import logging
from xml.dom import minidom

from gi.repository import Gtk

from syncthing_gtk.tools import _  # gettext function
from syncthing_gtk.tools import get_locale_dir

from .tools import GETTEXT_DOMAIN, IS_WINDOWS


log = logging.getLogger("UIBuilder")


class UIBuilder(Gtk.Builder):
    def __init__(self):
        Gtk.Builder.__init__(self)
        self.set_translation_domain(GETTEXT_DOMAIN)
        self.conditions = set([])
        self.icon_paths = []
        self.xml = None

    def add_from_file(self, filename):
        """ Builds UI from file """
        log.debug("Loading ui file %s", filename)
        if len(self.conditions) == 0 and not IS_WINDOWS and get_locale_dir() is None:
            # There is no need to do any magic in this case; Just use
            # Gtk.Builder directly
            Gtk.Builder.add_from_file(self, filename)
        else:
            with open(filename, "r") as f:
                self.add_from_string(f.read())

    def add_from_string(self, string):
        """ Builds UI from string """
        self.xml = minidom.parseString(string)
        self._build()

    def add_from_resource(self, *a):
        raise RuntimeError("add_from_resource is not supported")

    def enable_condition(self, *conds):
        """ Enables condition. Conditions are case-insensitive """
        for c in conds:
            log.debug("Enabled: %s", c)
            self.conditions.add(c)

    def disable_condition(self, *conds):
        """ Disables condition. Conditions are case-insensitive """
        for c in conds:
            log.debug("Disabled: %s", c)
            self.conditions.remove(c)

    def condition_met(self, cond):
        """
        Returns True if condition is met. Empty condition is True.
        Spaces at beginning or end of expressions are stripped.

        Supports simple |, & and !
        operators, but no parenthesis.
        (I just hope I'd never have to use them)
        """
        if "|" in cond:
            for sub in cond.split("|", 1):
                if self.condition_met(sub):
                    return True
            return False
        if "&" in cond:
            for sub in cond.split("&", 1):
                if not self.condition_met(sub):
                    return False
            return True
        if cond.strip().startswith("!"):
            return not self.condition_met(cond.strip()[1:])
        return cond.strip() in self.conditions

    def replace_icon_path(self, prefix, replace_with):
        """
        All path replaceaments defined using this method are applied
        by _build method on anything that remotely resembles icon path.
        """
        if not prefix.endswith("/"):
            prefix = "%s/" % (prefix,)
        if not replace_with.endswith("/"):
            replace_with = "%s/" % (replace_with,)
        self.icon_paths.append((prefix, replace_with))

    def _build(self):
        """
        Fun part starts here. Recursively walks through entire DOM tree,
        removes all <IF> tags replacing them with child nodes if when
        condition is met and fixes icon paths, if required.
        """
        log.debug("Enabled conditions: %s", self.conditions)
        self._replace_icon_paths(self.xml.documentElement)
        self._find_conditions(self.xml.documentElement)
        if IS_WINDOWS or get_locale_dir() is not None:
            self._find_translatables()
        # Now this will convert parsed DOM tree back to XML and fed it
        # to Gtk.Builder XML parser.
        # God probably kills kitten every time when method is called...
        Gtk.Builder.add_from_string(self, self.xml.toxml())

    def _find_translatables(self, node=None):
        """
        Fuck GTK devs.
        With cacti.
        https://bugzilla.gnome.org/show_bug.cgi?id=574520
        """
        if node is None:
            node = self.xml.documentElement
        for child in node.childNodes:
            if child.nodeType == child.ELEMENT_NODE:
                if child.tagName.lower() in ("property", "col"):
                    if child.getAttribute("translatable") == "yes":
                        self._translate(child)
                else:
                    self._find_translatables(child)

    def _translate(self, node):
        for child in node.childNodes:
            if child.nodeType == child.TEXT_NODE:
                child.nodeValue = _(child.nodeValue)

    def _replace_icon_paths(self, node):
        """ Recursive part for _build - icon paths """
        for child in node.childNodes:
            if child.nodeType == child.ELEMENT_NODE:
                self._replace_icon_paths(child)
                if child.tagName.lower() == "property":
                    if child.getAttribute("name") == "pixbuf":
                        # GtkImage, pixbuf path
                        self._check_icon_path(child)
                    elif child.getAttribute("name") == "icon":
                        # window or dialog, icon path
                        self._check_icon_path(child)

    def _find_conditions(self, node):
        """ Recursive part for _build - <IF> tags """
        for child in node.childNodes:
            if child.nodeType == child.ELEMENT_NODE:
                self._find_conditions(child)
                if child.tagName.lower() == "if":
                    self._solve_if_element(child)
                elif child.getAttribute("if") != "":
                    condition = child.getAttribute("if")
                    if not self.condition_met(condition):
                        log.debug("Removed '%s' by attribute: %s",
                                  child.tagName, condition)
                        node.removeChild(child)
                    else:
                        child.removeAttribute("if")

    def _solve_if_element(self, element):
        """
        Reads "condition" attribute and decides if condition is met
        Conditions are case-insensitive
        """
        condition = element.getAttribute("condition").lower().strip()
        if self.condition_met(condition):
            # Merge child nodes in place of this IF element
            # Remove ELSE elements, if any
            log.debug("Allowed node %s", condition)
            for elseem in getElementsByTagNameCI(element, "else"):
                element.removeChild(elseem)
            merge_with_parent(element, element)
        else:
            # Remove this element, but merge ELSE elements, if any
            log.debug("Removed node %s", condition)
            for elseem in getElementsByTagNameCI(element, "else"):
                merge_with_parent(elseem, element)
            element.parentNode.removeChild(element)

    def _check_icon_path(self, element):
        def replace(path):
            """
            If specified path begins with one of replaceament prefixes,
            returns path with modified prefix.
            """
            for prefix, replace_with in self.icon_paths:
                if path.startswith(prefix):
                    return "%s%s" % (replace_with, path[len(prefix):])
            return path

        for n in element.childNodes:
            if n.nodeType == n.TEXT_NODE:
                n.data = replace(n.data)
        return


def getElementsByTagNameCI(node, tagname):
    """
    Returns all elements with matching tag; Compares in
    case-insensitive way.
    """
    tagname = tagname.lower()
    return [child for child in node.childNodes if
            (child.nodeType == child.ELEMENT_NODE and
             child.tagName.lower() == tagname)
            ]


def merge_with_parent(element, insert_before):
    """ Merges child nodes with parent node """
    for child in element.childNodes:
        if child.nodeType == child.ELEMENT_NODE:
            element.removeChild(child)
            insert_before.parentNode.appendChild(child)
            insert_before.parentNode.insertBefore(child, insert_before)
    element.parentNode.removeChild(element)