Philipp Hörist pushed to branch master at gajim / gajim

Commits:

5 changed files:

Changes:

  • gajim/gtk/builder.py
    1
    +# This file is part of Gajim.
    
    2
    +#
    
    3
    +# Gajim is free software; you can redistribute it and/or modify
    
    4
    +# it under the terms of the GNU General Public License as published
    
    5
    +# by the Free Software Foundation; version 3 only.
    
    6
    +#
    
    7
    +# Gajim is distributed in the hope that it will be useful,
    
    8
    +# but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    9
    +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    
    10
    +# GNU General Public License for more details.
    
    11
    +#
    
    12
    +# You should have received a copy of the GNU General Public License
    
    13
    +# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
    
    14
    +
    
    15
    +from typing import Any
    
    16
    +from typing import Optional
    
    17
    +
    
    18
    +import sys
    
    19
    +import functools
    
    20
    +import xml.etree.ElementTree as ET
    
    21
    +
    
    22
    +from gi.repository import Gtk
    
    23
    +from gi.repository import Gio
    
    24
    +
    
    25
    +from gajim.common import i18n
    
    26
    +from gajim.common.i18n import _
    
    27
    +from gajim.common import configpaths
    
    28
    +
    
    29
    +
    
    30
    +class Builder:
    
    31
    +
    
    32
    +    filename = ''
    
    33
    +
    
    34
    +    def __init__(self,
    
    35
    +                 filename: Optional[str] = None,
    
    36
    +                 widgets: Optional[list[str]] = None,
    
    37
    +                 domain: Optional[str] = None,
    
    38
    +                 gettext_: Optional[Any] = None) -> None:
    
    39
    +
    
    40
    +        if filename is None:
    
    41
    +            filename = self.filename
    
    42
    +
    
    43
    +        self._builder = Gtk.Builder()
    
    44
    +
    
    45
    +        if domain is None:
    
    46
    +            domain = i18n.DOMAIN
    
    47
    +        self._builder.set_translation_domain(domain)
    
    48
    +
    
    49
    +        if gettext_ is None:
    
    50
    +            gettext_ = _
    
    51
    +
    
    52
    +        xml_text = self._load_string_from_filename(filename, gettext_)
    
    53
    +
    
    54
    +        if widgets is not None:
    
    55
    +            self._builder.add_objects_from_string(xml_text, widgets)
    
    56
    +        else:
    
    57
    +            self._builder.add_from_string(xml_text)
    
    58
    +
    
    59
    +    @staticmethod
    
    60
    +    @functools.lru_cache(maxsize=None)
    
    61
    +    def _load_string_from_filename(filename: str, gettext_: Any) -> str:
    
    62
    +        file_path = str(configpaths.get('GUI') / filename)
    
    63
    +
    
    64
    +        if sys.platform == "win32":
    
    65
    +            # This is a workaround for non working translation on Windows
    
    66
    +            tree = ET.parse(file_path)
    
    67
    +            for node in tree.iter():
    
    68
    +                if 'translatable' in node.attrib and node.text is not None:
    
    69
    +                    node.text = gettext_(node.text)
    
    70
    +
    
    71
    +            return ET.tostring(tree.getroot(),
    
    72
    +                               encoding='unicode',
    
    73
    +                               method='xml')
    
    74
    +
    
    75
    +        file = Gio.File.new_for_path(file_path)
    
    76
    +        content = file.load_contents(None)
    
    77
    +        return content[1].decode()
    
    78
    +
    
    79
    +    def __getattr__(self, name: str) -> Any:
    
    80
    +        try:
    
    81
    +            return getattr(self._builder, name)
    
    82
    +        except AttributeError:
    
    83
    +            return self._builder.get_object(name)
    
    84
    +
    
    85
    +    def get(self, name: str) -> Any:
    
    86
    +        return self._builder.get_object(name)
    
    87
    +
    
    88
    +
    
    89
    +class SearchViewBuilder(Builder):
    
    90
    +    filename = 'search_view.ui'

  • gajim/gtk/builder.pyi
    1
    +
    
    2
    +from typing import Any
    
    3
    +from typing import Literal
    
    4
    +from typing import overload
    
    5
    +
    
    6
    +from gi.repository import Gtk
    
    7
    +
    
    8
    +
    
    9
    +
    
    10
    +class Builder: ...
    
    11
    +
    
    12
    +
    
    13
    +class SearchViewBuilder(Builder):
    
    14
    +
    
    15
    +    filename: str = ...
    
    16
    +
    
    17
    +    @overload
    
    18
    +    def get(self, name: Literal['search_entry']) -> Gtk.Entry: ...
    
    19
    +    @overload
    
    20
    +    def get(self, name: Literal['date_hint']) -> Gtk.Label: ...
    
    21
    +    @overload
    
    22
    +    def get(self, name: Literal['search_checkbutton']) -> Gtk.CheckButton: ...
    
    23
    +    @overload
    
    24
    +    def get(self, name: Literal['header_box']) -> Gtk.ListBox: ...
    
    25
    +    @overload
    
    26
    +    def get(self, name: Literal['header_name_label']) -> Gtk.Label: ...
    
    27
    +    @overload
    
    28
    +    def get(self, name: Literal['header_date_label']) -> Gtk.Label: ...
    
    29
    +    @overload
    
    30
    +    def get(self, name: Literal['result_row_grid']) -> Gtk.Grid: ...
    
    31
    +    @overload
    
    32
    +    def get(self, name: Literal['row_name_label']) -> Gtk.Label: ...
    
    33
    +    @overload
    
    34
    +    def get(self, name: Literal['row_avatar']) -> Gtk.Image: ...
    
    35
    +    @overload
    
    36
    +    def get(self, name: Literal['row_time_label']) -> Gtk.Label: ...
    
    37
    +    @overload
    
    38
    +    def get(self, name: Literal['results_listbox']) -> Gtk.ListBox: ...
    
    39
    +    @overload
    
    40
    +    def get(self, name: Literal['search_box']) -> Gtk.Box: ...
    
    41
    +    def get(self, name: str) -> Any:
    
    42
    +        pass

  • gajim/gtk/conversation/code_widget.py
    ... ... @@ -98,6 +98,7 @@ def __init__(self) -> None:
    98 98
             self.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
    
    99 99
             self.set_top_margin(2)
    
    100 100
             self.set_bottom_margin(2)
    
    101
    +        self.set_monospace(True)
    
    101 102
     
    
    102 103
             self._source_manager = GtkSource.LanguageManager.get_default()
    
    103 104
             self._style_scheme_manager = GtkSource.StyleSchemeManager.get_default()
    

  • gajim/gtk/search_view.py
    ... ... @@ -12,16 +12,16 @@
    12 12
     # You should have received a copy of the GNU General Public License
    
    13 13
     # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
    
    14 14
     
    
    15
    -from typing import Dict
    
    16
    -from typing import List
    
    15
    +from __future__ import annotations
    
    16
    +
    
    17
    +from typing import Any
    
    18
    +from typing import Union
    
    17 19
     from typing import Optional
    
    18
    -from typing import Tuple
    
    19 20
     
    
    20 21
     import datetime
    
    21 22
     import logging
    
    22 23
     import time
    
    23 24
     import re
    
    24
    -from collections import namedtuple
    
    25 25
     
    
    26 26
     from gi.repository import GObject
    
    27 27
     from gi.repository import Gtk
    
    ... ... @@ -37,7 +37,7 @@
    37 37
     from gajim.common.i18n import _
    
    38 38
     
    
    39 39
     from .conversation.message_widget import MessageWidget
    
    40
    -from .util import get_builder
    
    40
    +from .builder import SearchViewBuilder
    
    41 41
     
    
    42 42
     log = logging.getLogger('gajim.gui.search_view')
    
    43 43
     
    
    ... ... @@ -56,12 +56,12 @@ def __init__(self) -> None:
    56 56
     
    
    57 57
             self._account: Optional[str] = None
    
    58 58
             self._jid: Optional[JID] = None
    
    59
    -        self._results: List[str] = []
    
    59
    +        self._results: list[str] = []
    
    60 60
             self._scope: Optional[str] = None
    
    61 61
     
    
    62
    -        self._ui = get_builder('search_view.ui')
    
    63
    -        self._ui.results_listbox.set_header_func(self._header_func)
    
    64
    -        self.add(self._ui.search_box)
    
    62
    +        self._ui = SearchViewBuilder()
    
    63
    +        self._ui.get('results_listbox').set_header_func(self._header_func)
    
    64
    +        self.add(self._ui.get('search_box'))
    
    65 65
     
    
    66 66
             self._ui.connect_signals(self)
    
    67 67
     
    
    ... ... @@ -73,13 +73,15 @@ def __init__(self) -> None:
    73 73
                                            self._on_account_state)
    
    74 74
             self.show_all()
    
    75 75
     
    
    76
    -    def _on_account_state(self, _event):
    
    76
    +    def _on_account_state(self, _event: Any) -> None:
    
    77 77
             self.clear()
    
    78 78
     
    
    79 79
         @staticmethod
    
    80
    -    def _header_func(row, before):
    
    80
    +    def _header_func(row: Union[CounterRow, ResultRow],
    
    81
    +                     before: Optional[Union[CounterRow, ResultRow]]) -> None:
    
    82
    +
    
    81 83
             if before is None:
    
    82
    -            if row.type == 'counter':
    
    84
    +            if isinstance(row, CounterRow):
    
    83 85
                     row.set_header(None)
    
    84 86
                 else:
    
    85 87
                     row.set_header(RowHeader(row.account, row.jid, row.time))
    
    ... ... @@ -97,17 +99,17 @@ def _on_hide_clicked(self, _button: Gtk.Button) -> None:
    97 99
             self.emit('hide-search')
    
    98 100
     
    
    99 101
         def clear(self) -> None:
    
    100
    -        self._ui.search_entry.set_text('')
    
    102
    +        self._ui.get('search_entry').set_text('')
    
    101 103
             self._clear_results()
    
    102 104
     
    
    103 105
         def _clear_results(self) -> None:
    
    104
    -        for row in self._ui.results_listbox.get_children():
    
    105
    -            self._ui.results_listbox.remove(row)
    
    106
    +        for row in self._ui.get('results_listbox').get_children():
    
    107
    +            self._ui.get('results_listbox').remove(row)
    
    106 108
                 row.destroy()
    
    107 109
     
    
    108 110
         def _on_search(self, entry: Gtk.Entry) -> None:
    
    109 111
             self._clear_results()
    
    110
    -        self._ui.date_hint.hide()
    
    112
    +        self._ui.get('date_hint').hide()
    
    111 113
             text = entry.get_text()
    
    112 114
             if not text:
    
    113 115
                 return
    
    ... ... @@ -124,7 +126,7 @@ def _on_search(self, entry: Gtk.Entry) -> None:
    124 126
                     before_filters = min([datetime.datetime.fromisoformat(date) for
    
    125 127
                                           date in before_filters])
    
    126 128
                 except ValueError:
    
    127
    -                self._ui.date_hint.show()
    
    129
    +                self._ui.get('date_hint').show()
    
    128 130
                     return
    
    129 131
     
    
    130 132
             # after:date
    
    ... ... @@ -140,13 +142,13 @@ def _on_search(self, entry: Gtk.Entry) -> None:
    140 142
                     if after_filters.hour == after_filters.minute == 0:
    
    141 143
                         after_filters += datetime.timedelta(days=1)
    
    142 144
                 except ValueError:
    
    143
    -                self._ui.date_hint.show()
    
    145
    +                self._ui.get('date_hint').show()
    
    144 146
                     return
    
    145 147
     
    
    146 148
             # has:'file'|'img'|'video'|filetype
    
    147 149
             text, has_filters = self._strip_filters(text, 'has')
    
    148 150
     
    
    149
    -        everywhere = self._ui.search_checkbutton.get_active()
    
    151
    +        everywhere = self._ui.get('search_checkbutton').get_active()
    
    150 152
             context = self._account is not None and self._jid is not None
    
    151 153
     
    
    152 154
             if not context or everywhere:
    
    ... ... @@ -167,7 +169,7 @@ def _on_search(self, entry: Gtk.Entry) -> None:
    167 169
                     after=after_filters)
    
    168 170
     
    
    169 171
             if has_filters is not None:
    
    170
    -            filetypes = []
    
    172
    +            filetypes: list[str] = []
    
    171 173
                 for filetype in has_filters:
    
    172 174
                     types = FILE_CATEGORIES.get(filetype)
    
    173 175
                     if types is None:
    
    ... ... @@ -180,9 +182,7 @@ def _on_search(self, entry: Gtk.Entry) -> None:
    180 182
             self._add_counter()
    
    181 183
             self._add_results()
    
    182 184
     
    
    183
    -    def _filter_results_for_files(self,
    
    184
    -                                  filetypes: List[str]
    
    185
    -                                  ) -> None:
    
    185
    +    def _filter_results_for_files(self, filetypes: list[str]) -> None:
    
    186 186
             if 'file' in filetypes:
    
    187 187
                 results = []
    
    188 188
                 for result in self._results:
    
    ... ... @@ -202,9 +202,8 @@ def _filter_results_for_files(self,
    202 202
     
    
    203 203
         @staticmethod
    
    204 204
         def _strip_filters(text: str,
    
    205
    -                       filter_name: str
    
    206
    -                       ) -> Tuple[str, Optional[List[str]]]:
    
    207
    -        filters = []
    
    205
    +                       filter_name: str) -> tuple[str, Optional[list[str]]]:
    
    206
    +        filters: list[str] = []
    
    208 207
             start = 0
    
    209 208
             new_text = ''
    
    210 209
             for search_filter in re.finditer(filter_name + r':(\S+)\s?', text):
    
    ... ... @@ -218,7 +217,7 @@ def _strip_filters(text: str,
    218 217
         def _add_counter(self) -> None:
    
    219 218
             results_count = len(self._results)
    
    220 219
             if results_count:
    
    221
    -            self._ui.results_listbox.add(CounterRow(results_count))
    
    220
    +            self._ui.get('results_listbox').add(CounterRow(results_count))
    
    222 221
     
    
    223 222
         def _add_results(self) -> None:
    
    224 223
             accounts = self._get_accounts()
    
    ... ... @@ -231,31 +230,27 @@ def _add_results(self) -> None:
    231 230
                 else:
    
    232 231
                     result_row = ResultRow(msg, self._account, self._jid)
    
    233 232
     
    
    234
    -            self._ui.results_listbox.add(result_row)
    
    233
    +            self._ui.get('results_listbox').add(result_row)
    
    235 234
             self._results = self._results[25:]
    
    236 235
     
    
    237 236
         def _on_edge_reached(self,
    
    238 237
                              _scrolledwin: Gtk.ScrolledWindow,
    
    239
    -                         pos: Gtk.PositionType
    
    240
    -                         ) -> None:
    
    238
    +                         pos: Gtk.PositionType) -> None:
    
    241 239
             if pos != Gtk.PositionType.BOTTOM:
    
    242 240
                 return
    
    243 241
     
    
    244 242
             self._add_results()
    
    245 243
     
    
    246 244
         @staticmethod
    
    247
    -    def _get_accounts() -> Dict[str, str]:
    
    248
    -        accounts = {}
    
    245
    +    def _get_accounts() -> dict[str, str]:
    
    246
    +        accounts: dict[str, str] = {}
    
    249 247
             for account in app.settings.get_accounts():
    
    250 248
                 account_id = app.storage.archive.get_account_id(account)
    
    251 249
                 accounts[account_id] = account
    
    252 250
             return accounts
    
    253 251
     
    
    254 252
         @staticmethod
    
    255
    -    def _on_row_activated(_listbox: Gtk.ListBox, row: Gtk.ListBoxRow) -> None:
    
    256
    -        if row.type == 'counter':
    
    257
    -            return
    
    258
    -
    
    253
    +    def _on_row_activated(_listbox: SearchView, row: ResultRow) -> None:
    
    259 254
             control = app.window.get_active_control()
    
    260 255
             if control is not None:
    
    261 256
                 if control.contact.jid == row.jid:
    
    ... ... @@ -271,12 +266,12 @@ def _on_row_activated(_listbox: Gtk.ListBox, row: Gtk.ListBoxRow) -> None:
    271 266
                 control.scroll_to_message(row.log_line_id, row.timestamp)
    
    272 267
     
    
    273 268
         def set_focus(self) -> None:
    
    274
    -        self._ui.search_entry.grab_focus()
    
    269
    +        self._ui.get('search_entry').grab_focus()
    
    275 270
     
    
    276 271
         def set_context(self, account: str, jid: JID) -> None:
    
    277 272
             self._account = account
    
    278 273
             self._jid = jid
    
    279
    -        self._ui.search_checkbutton.set_active(jid is None)
    
    274
    +        self._ui.get('search_checkbutton').set_active(jid is None)
    
    280 275
     
    
    281 276
     
    
    282 277
     class RowHeader(Gtk.Box):
    
    ... ... @@ -284,16 +279,16 @@ def __init__(self, account: str, jid: JID, timestamp: float) -> None:
    284 279
             Gtk.Box.__init__(self)
    
    285 280
             self.set_hexpand(True)
    
    286 281
     
    
    287
    -        self._ui = get_builder('search_view.ui')
    
    288
    -        self.add(self._ui.header_box)
    
    282
    +        self._ui = SearchViewBuilder()
    
    283
    +        self.add(self._ui.get('header_box'))
    
    289 284
     
    
    290 285
             client = app.get_client(account)
    
    291 286
             contact = client.get_module('Contacts').get_contact(jid)
    
    292
    -        self._ui.header_name_label.set_text(contact.name or '')
    
    287
    +        self._ui.get('header_name_label').set_text(contact.name or '')
    
    293 288
     
    
    294 289
             local_time = time.localtime(timestamp)
    
    295 290
             date = time.strftime('%x', local_time)
    
    296
    -        self._ui.header_date_label.set_text(date)
    
    291
    +        self._ui.get('header_date_label').set_text(date)
    
    297 292
     
    
    298 293
             self.show_all()
    
    299 294
     
    
    ... ... @@ -302,9 +297,9 @@ class CounterRow(Gtk.ListBoxRow):
    302 297
         def __init__(self, count: int) -> None:
    
    303 298
             Gtk.ListBoxRow.__init__(self)
    
    304 299
             self.set_activatable(False)
    
    305
    -        self.type: str = 'counter'
    
    306
    -        self.jid: str = ''  # Has to be there for header_func
    
    307
    -        self.time: float = 0.0
    
    300
    +        self.type = 'counter'
    
    301
    +        self.jid = ''  # Has to be there for header_func
    
    302
    +        self.time = 0.0
    
    308 303
             self.get_style_context().add_class('search-view-counter')
    
    309 304
     
    
    310 305
             if count == 1:
    
    ... ... @@ -317,7 +312,7 @@ def __init__(self, count: int) -> None:
    317 312
     
    
    318 313
     
    
    319 314
     class ResultRow(Gtk.ListBoxRow):
    
    320
    -    def __init__(self, msg: namedtuple, account: str, jid: JID) -> None:
    
    315
    +    def __init__(self, msg: Any, account: str, jid: JID) -> None:
    
    321 316
             Gtk.ListBoxRow.__init__(self)
    
    322 317
             self.account = account
    
    323 318
             self.jid = jid
    
    ... ... @@ -335,8 +330,8 @@ def __init__(self, msg: namedtuple, account: str, jid: JID) -> None:
    335 330
             self.contact = self._client.get_module('Contacts').get_contact(
    
    336 331
                 jid, groupchat=self.type == 'groupchat')
    
    337 332
     
    
    338
    -        self._ui = get_builder('search_view.ui')
    
    339
    -        self.add(self._ui.result_row_grid)
    
    333
    +        self._ui = SearchViewBuilder()
    
    334
    +        self.add(self._ui.get('result_row_grid'))
    
    340 335
     
    
    341 336
             kind = 'status'
    
    342 337
             contact_name = msg.contact_name
    
    ... ... @@ -350,25 +345,25 @@ def __init__(self, msg: namedtuple, account: str, jid: JID) -> None:
    350 345
                     KindConstant.SINGLE_MSG_SENT, KindConstant.CHAT_MSG_SENT):
    
    351 346
                 kind = 'outgoing'
    
    352 347
                 contact_name = app.nicks[account]
    
    353
    -        self._ui.row_name_label.set_text(contact_name)
    
    348
    +        self._ui.get('row_name_label').set_text(contact_name)
    
    354 349
     
    
    355 350
             avatar = self._get_avatar(kind, contact_name)
    
    356
    -        self._ui.row_avatar.set_from_surface(avatar)
    
    351
    +        self._ui.get('row_avatar').set_from_surface(avatar)
    
    357 352
     
    
    358 353
             local_time = time.localtime(msg.time)
    
    359 354
             date = time.strftime('%H:%M', local_time)
    
    360
    -        self._ui.row_time_label.set_label(date)
    
    355
    +        self._ui.get('row_time_label').set_label(date)
    
    361 356
     
    
    362 357
             message_widget = MessageWidget(account, selectable=False)
    
    363 358
             message_widget.add_with_styling(msg.message, nickname=contact_name)
    
    364
    -        self._ui.result_row_grid.attach(message_widget, 1, 1, 2, 1)
    
    359
    +        self._ui.get('result_row_grid').attach(message_widget, 1, 1, 2, 1)
    
    365 360
     
    
    366 361
             self.show_all()
    
    367 362
     
    
    368 363
         def _get_avatar(self,
    
    369 364
                         kind: str,
    
    370
    -                    name: str
    
    371
    -                    ) -> Optional[cairo.ImageSurface]:
    
    365
    +                    name: str) -> Optional[cairo.ImageSurface]:
    
    366
    +
    
    372 367
             scale = self.get_scale_factor()
    
    373 368
             if self.contact.is_groupchat:
    
    374 369
                 contact = self.contact.get_resource(name)
    

  • gajim/gtk/util.py
    ... ... @@ -21,14 +21,11 @@
    21 21
     from typing import Optional
    
    22 22
     from typing import Union
    
    23 23
     
    
    24
    -import sys
    
    25 24
     import logging
    
    26 25
     import math
    
    27 26
     import textwrap
    
    28
    -import functools
    
    29 27
     from io import BytesIO
    
    30 28
     from importlib import import_module
    
    31
    -import xml.etree.ElementTree as ET
    
    32 29
     from functools import wraps
    
    33 30
     from functools import lru_cache
    
    34 31
     
    
    ... ... @@ -40,7 +37,6 @@
    40 37
     from gi.repository import Gdk
    
    41 38
     from gi.repository import Gtk
    
    42 39
     from gi.repository import GLib
    
    43
    -from gi.repository import Gio
    
    44 40
     from gi.repository import Pango
    
    45 41
     from gi.repository import GdkPixbuf
    
    46 42
     import nbxmpp
    
    ... ... @@ -48,7 +44,6 @@
    48 44
     
    
    49 45
     from gajim.common import app
    
    50 46
     from gajim.common import configpaths
    
    51
    -from gajim.common import i18n
    
    52 47
     from gajim.common.i18n import _
    
    53 48
     from gajim.common.helpers import URL_REGEX
    
    54 49
     from gajim.common.const import LOCATION_DATA
    
    ... ... @@ -59,11 +54,13 @@
    59 54
     
    
    60 55
     from .const import GajimIconSet
    
    61 56
     from .const import WINDOW_MODULES
    
    57
    +from .builder import Builder
    
    62 58
     
    
    63 59
     _icon_theme = Gtk.IconTheme.get_default()
    
    64 60
     if _icon_theme is not None:
    
    65 61
         _icon_theme.append_search_path(str(configpaths.get('ICONS')))
    
    66 62
     
    
    63
    +
    
    67 64
     log = logging.getLogger('gajim.gui.util')
    
    68 65
     
    
    69 66
     
    
    ... ... @@ -150,55 +147,6 @@ def nick_matching(nick: str) -> bool:
    150 147
             return matches + other_nicks
    
    151 148
     
    
    152 149
     
    
    153
    -class Builder:
    
    154
    -    def __init__(self,
    
    155
    -                 filename: str,
    
    156
    -                 widgets: List[str] = None,
    
    157
    -                 domain: str = None,
    
    158
    -                 gettext_: Any = None) -> None:
    
    159
    -        self._builder = Gtk.Builder()
    
    160
    -
    
    161
    -        if domain is None:
    
    162
    -            domain = i18n.DOMAIN
    
    163
    -        self._builder.set_translation_domain(domain)
    
    164
    -
    
    165
    -        if gettext_ is None:
    
    166
    -            gettext_ = _
    
    167
    -
    
    168
    -        xml_text = self._load_string_from_filename(filename, gettext_)
    
    169
    -
    
    170
    -        if widgets is not None:
    
    171
    -            self._builder.add_objects_from_string(xml_text, widgets)
    
    172
    -        else:
    
    173
    -            self._builder.add_from_string(xml_text)
    
    174
    -
    
    175
    -    @staticmethod
    
    176
    -    @functools.lru_cache(maxsize=None)
    
    177
    -    def _load_string_from_filename(filename, gettext_):
    
    178
    -        file_path = str(configpaths.get('GUI') / filename)
    
    179
    -
    
    180
    -        if sys.platform == "win32":
    
    181
    -            # This is a workaround for non working translation on Windows
    
    182
    -            tree = ET.parse(file_path)
    
    183
    -            for node in tree.iter():
    
    184
    -                if 'translatable' in node.attrib and node.text is not None:
    
    185
    -                    node.text = gettext_(node.text)
    
    186
    -
    
    187
    -            return ET.tostring(tree.getroot(),
    
    188
    -                               encoding='unicode',
    
    189
    -                               method='xml')
    
    190
    -
    
    191
    -        file = Gio.File.new_for_path(file_path)
    
    192
    -        content = file.load_contents(None)
    
    193
    -        return content[1].decode()
    
    194
    -
    
    195
    -    def __getattr__(self, name):
    
    196
    -        try:
    
    197
    -            return getattr(self._builder, name)
    
    198
    -        except AttributeError:
    
    199
    -            return self._builder.get_object(name)
    
    200
    -
    
    201
    -
    
    202 150
     def get_builder(file_name: str, widgets: List[str] = None) -> Builder:
    
    203 151
         return Builder(file_name, widgets)
    
    204 152