Philipp Hörist pushed to branch master at gajim / gajim
Commits: 1f22acaa by lovetox at 2021-12-18T00:42:36+01:00 CodeWidget: Set monospace
- - - - - fa918de2 by lovetox at 2021-12-18T00:42:36+01:00 Move Builder to own module
- Add type hints
- - - - - 489659df by lovetox at 2021-12-18T00:42:36+01:00 SearchView: Use type aware builder
- - - - -
5 changed files:
- + gajim/gtk/builder.py - + gajim/gtk/builder.pyi - gajim/gtk/conversation/code_widget.py - gajim/gtk/search_view.py - gajim/gtk/util.py
Changes:
===================================== gajim/gtk/builder.py ===================================== @@ -0,0 +1,90 @@ +# This file is part of Gajim. +# +# Gajim 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; version 3 only. +# +# Gajim 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 Gajim. If not, see http://www.gnu.org/licenses/. + +from typing import Any +from typing import Optional + +import sys +import functools +import xml.etree.ElementTree as ET + +from gi.repository import Gtk +from gi.repository import Gio + +from gajim.common import i18n +from gajim.common.i18n import _ +from gajim.common import configpaths + + +class Builder: + + filename = '' + + def __init__(self, + filename: Optional[str] = None, + widgets: Optional[list[str]] = None, + domain: Optional[str] = None, + gettext_: Optional[Any] = None) -> None: + + if filename is None: + filename = self.filename + + self._builder = Gtk.Builder() + + if domain is None: + domain = i18n.DOMAIN + self._builder.set_translation_domain(domain) + + if gettext_ is None: + gettext_ = _ + + xml_text = self._load_string_from_filename(filename, gettext_) + + if widgets is not None: + self._builder.add_objects_from_string(xml_text, widgets) + else: + self._builder.add_from_string(xml_text) + + @staticmethod + @functools.lru_cache(maxsize=None) + def _load_string_from_filename(filename: str, gettext_: Any) -> str: + file_path = str(configpaths.get('GUI') / filename) + + if sys.platform == "win32": + # This is a workaround for non working translation on Windows + tree = ET.parse(file_path) + for node in tree.iter(): + if 'translatable' in node.attrib and node.text is not None: + node.text = gettext_(node.text) + + return ET.tostring(tree.getroot(), + encoding='unicode', + method='xml') + + file = Gio.File.new_for_path(file_path) + content = file.load_contents(None) + return content[1].decode() + + def __getattr__(self, name: str) -> Any: + try: + return getattr(self._builder, name) + except AttributeError: + return self._builder.get_object(name) + + def get(self, name: str) -> Any: + return self._builder.get_object(name) + + +class SearchViewBuilder(Builder): + filename = 'search_view.ui'
===================================== gajim/gtk/builder.pyi ===================================== @@ -0,0 +1,42 @@ + +from typing import Any +from typing import Literal +from typing import overload + +from gi.repository import Gtk + + + +class Builder: ... + + +class SearchViewBuilder(Builder): + + filename: str = ... + + @overload + def get(self, name: Literal['search_entry']) -> Gtk.Entry: ... + @overload + def get(self, name: Literal['date_hint']) -> Gtk.Label: ... + @overload + def get(self, name: Literal['search_checkbutton']) -> Gtk.CheckButton: ... + @overload + def get(self, name: Literal['header_box']) -> Gtk.ListBox: ... + @overload + def get(self, name: Literal['header_name_label']) -> Gtk.Label: ... + @overload + def get(self, name: Literal['header_date_label']) -> Gtk.Label: ... + @overload + def get(self, name: Literal['result_row_grid']) -> Gtk.Grid: ... + @overload + def get(self, name: Literal['row_name_label']) -> Gtk.Label: ... + @overload + def get(self, name: Literal['row_avatar']) -> Gtk.Image: ... + @overload + def get(self, name: Literal['row_time_label']) -> Gtk.Label: ... + @overload + def get(self, name: Literal['results_listbox']) -> Gtk.ListBox: ... + @overload + def get(self, name: Literal['search_box']) -> Gtk.Box: ... + def get(self, name: str) -> Any: + pass
===================================== gajim/gtk/conversation/code_widget.py ===================================== @@ -98,6 +98,7 @@ def __init__(self) -> None: self.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) self.set_top_margin(2) self.set_bottom_margin(2) + self.set_monospace(True)
self._source_manager = GtkSource.LanguageManager.get_default() self._style_scheme_manager = GtkSource.StyleSchemeManager.get_default()
===================================== gajim/gtk/search_view.py ===================================== @@ -12,16 +12,16 @@ # You should have received a copy of the GNU General Public License # along with Gajim. If not, see http://www.gnu.org/licenses/.
-from typing import Dict -from typing import List +from __future__ import annotations + +from typing import Any +from typing import Union from typing import Optional -from typing import Tuple
import datetime import logging import time import re -from collections import namedtuple
from gi.repository import GObject from gi.repository import Gtk @@ -37,7 +37,7 @@ from gajim.common.i18n import _
from .conversation.message_widget import MessageWidget -from .util import get_builder +from .builder import SearchViewBuilder
log = logging.getLogger('gajim.gui.search_view')
@@ -56,12 +56,12 @@ def __init__(self) -> None:
self._account: Optional[str] = None self._jid: Optional[JID] = None - self._results: List[str] = [] + self._results: list[str] = [] self._scope: Optional[str] = None
- self._ui = get_builder('search_view.ui') - self._ui.results_listbox.set_header_func(self._header_func) - self.add(self._ui.search_box) + self._ui = SearchViewBuilder() + self._ui.get('results_listbox').set_header_func(self._header_func) + self.add(self._ui.get('search_box'))
self._ui.connect_signals(self)
@@ -73,13 +73,15 @@ def __init__(self) -> None: self._on_account_state) self.show_all()
- def _on_account_state(self, _event): + def _on_account_state(self, _event: Any) -> None: self.clear()
@staticmethod - def _header_func(row, before): + def _header_func(row: Union[CounterRow, ResultRow], + before: Optional[Union[CounterRow, ResultRow]]) -> None: + if before is None: - if row.type == 'counter': + if isinstance(row, CounterRow): row.set_header(None) else: row.set_header(RowHeader(row.account, row.jid, row.time)) @@ -97,17 +99,17 @@ def _on_hide_clicked(self, _button: Gtk.Button) -> None: self.emit('hide-search')
def clear(self) -> None: - self._ui.search_entry.set_text('') + self._ui.get('search_entry').set_text('') self._clear_results()
def _clear_results(self) -> None: - for row in self._ui.results_listbox.get_children(): - self._ui.results_listbox.remove(row) + for row in self._ui.get('results_listbox').get_children(): + self._ui.get('results_listbox').remove(row) row.destroy()
def _on_search(self, entry: Gtk.Entry) -> None: self._clear_results() - self._ui.date_hint.hide() + self._ui.get('date_hint').hide() text = entry.get_text() if not text: return @@ -124,7 +126,7 @@ def _on_search(self, entry: Gtk.Entry) -> None: before_filters = min([datetime.datetime.fromisoformat(date) for date in before_filters]) except ValueError: - self._ui.date_hint.show() + self._ui.get('date_hint').show() return
# after:date @@ -140,13 +142,13 @@ def _on_search(self, entry: Gtk.Entry) -> None: if after_filters.hour == after_filters.minute == 0: after_filters += datetime.timedelta(days=1) except ValueError: - self._ui.date_hint.show() + self._ui.get('date_hint').show() return
# has:'file'|'img'|'video'|filetype text, has_filters = self._strip_filters(text, 'has')
- everywhere = self._ui.search_checkbutton.get_active() + everywhere = self._ui.get('search_checkbutton').get_active() context = self._account is not None and self._jid is not None
if not context or everywhere: @@ -167,7 +169,7 @@ def _on_search(self, entry: Gtk.Entry) -> None: after=after_filters)
if has_filters is not None: - filetypes = [] + filetypes: list[str] = [] for filetype in has_filters: types = FILE_CATEGORIES.get(filetype) if types is None: @@ -180,9 +182,7 @@ def _on_search(self, entry: Gtk.Entry) -> None: self._add_counter() self._add_results()
- def _filter_results_for_files(self, - filetypes: List[str] - ) -> None: + def _filter_results_for_files(self, filetypes: list[str]) -> None: if 'file' in filetypes: results = [] for result in self._results: @@ -202,9 +202,8 @@ def _filter_results_for_files(self,
@staticmethod def _strip_filters(text: str, - filter_name: str - ) -> Tuple[str, Optional[List[str]]]: - filters = [] + filter_name: str) -> tuple[str, Optional[list[str]]]: + filters: list[str] = [] start = 0 new_text = '' for search_filter in re.finditer(filter_name + r':(\S+)\s?', text): @@ -218,7 +217,7 @@ def _strip_filters(text: str, def _add_counter(self) -> None: results_count = len(self._results) if results_count: - self._ui.results_listbox.add(CounterRow(results_count)) + self._ui.get('results_listbox').add(CounterRow(results_count))
def _add_results(self) -> None: accounts = self._get_accounts() @@ -231,31 +230,27 @@ def _add_results(self) -> None: else: result_row = ResultRow(msg, self._account, self._jid)
- self._ui.results_listbox.add(result_row) + self._ui.get('results_listbox').add(result_row) self._results = self._results[25:]
def _on_edge_reached(self, _scrolledwin: Gtk.ScrolledWindow, - pos: Gtk.PositionType - ) -> None: + pos: Gtk.PositionType) -> None: if pos != Gtk.PositionType.BOTTOM: return
self._add_results()
@staticmethod - def _get_accounts() -> Dict[str, str]: - accounts = {} + def _get_accounts() -> dict[str, str]: + accounts: dict[str, str] = {} for account in app.settings.get_accounts(): account_id = app.storage.archive.get_account_id(account) accounts[account_id] = account return accounts
@staticmethod - def _on_row_activated(_listbox: Gtk.ListBox, row: Gtk.ListBoxRow) -> None: - if row.type == 'counter': - return - + def _on_row_activated(_listbox: SearchView, row: ResultRow) -> None: control = app.window.get_active_control() if control is not None: if control.contact.jid == row.jid: @@ -271,12 +266,12 @@ def _on_row_activated(_listbox: Gtk.ListBox, row: Gtk.ListBoxRow) -> None: control.scroll_to_message(row.log_line_id, row.timestamp)
def set_focus(self) -> None: - self._ui.search_entry.grab_focus() + self._ui.get('search_entry').grab_focus()
def set_context(self, account: str, jid: JID) -> None: self._account = account self._jid = jid - self._ui.search_checkbutton.set_active(jid is None) + self._ui.get('search_checkbutton').set_active(jid is None)
class RowHeader(Gtk.Box): @@ -284,16 +279,16 @@ def __init__(self, account: str, jid: JID, timestamp: float) -> None: Gtk.Box.__init__(self) self.set_hexpand(True)
- self._ui = get_builder('search_view.ui') - self.add(self._ui.header_box) + self._ui = SearchViewBuilder() + self.add(self._ui.get('header_box'))
client = app.get_client(account) contact = client.get_module('Contacts').get_contact(jid) - self._ui.header_name_label.set_text(contact.name or '') + self._ui.get('header_name_label').set_text(contact.name or '')
local_time = time.localtime(timestamp) date = time.strftime('%x', local_time) - self._ui.header_date_label.set_text(date) + self._ui.get('header_date_label').set_text(date)
self.show_all()
@@ -302,9 +297,9 @@ class CounterRow(Gtk.ListBoxRow): def __init__(self, count: int) -> None: Gtk.ListBoxRow.__init__(self) self.set_activatable(False) - self.type: str = 'counter' - self.jid: str = '' # Has to be there for header_func - self.time: float = 0.0 + self.type = 'counter' + self.jid = '' # Has to be there for header_func + self.time = 0.0 self.get_style_context().add_class('search-view-counter')
if count == 1: @@ -317,7 +312,7 @@ def __init__(self, count: int) -> None:
class ResultRow(Gtk.ListBoxRow): - def __init__(self, msg: namedtuple, account: str, jid: JID) -> None: + def __init__(self, msg: Any, account: str, jid: JID) -> None: Gtk.ListBoxRow.__init__(self) self.account = account self.jid = jid @@ -335,8 +330,8 @@ def __init__(self, msg: namedtuple, account: str, jid: JID) -> None: self.contact = self._client.get_module('Contacts').get_contact( jid, groupchat=self.type == 'groupchat')
- self._ui = get_builder('search_view.ui') - self.add(self._ui.result_row_grid) + self._ui = SearchViewBuilder() + self.add(self._ui.get('result_row_grid'))
kind = 'status' contact_name = msg.contact_name @@ -350,25 +345,25 @@ def __init__(self, msg: namedtuple, account: str, jid: JID) -> None: KindConstant.SINGLE_MSG_SENT, KindConstant.CHAT_MSG_SENT): kind = 'outgoing' contact_name = app.nicks[account] - self._ui.row_name_label.set_text(contact_name) + self._ui.get('row_name_label').set_text(contact_name)
avatar = self._get_avatar(kind, contact_name) - self._ui.row_avatar.set_from_surface(avatar) + self._ui.get('row_avatar').set_from_surface(avatar)
local_time = time.localtime(msg.time) date = time.strftime('%H:%M', local_time) - self._ui.row_time_label.set_label(date) + self._ui.get('row_time_label').set_label(date)
message_widget = MessageWidget(account, selectable=False) message_widget.add_with_styling(msg.message, nickname=contact_name) - self._ui.result_row_grid.attach(message_widget, 1, 1, 2, 1) + self._ui.get('result_row_grid').attach(message_widget, 1, 1, 2, 1)
self.show_all()
def _get_avatar(self, kind: str, - name: str - ) -> Optional[cairo.ImageSurface]: + name: str) -> Optional[cairo.ImageSurface]: + scale = self.get_scale_factor() if self.contact.is_groupchat: contact = self.contact.get_resource(name)
===================================== gajim/gtk/util.py ===================================== @@ -21,14 +21,11 @@ from typing import Optional from typing import Union
-import sys import logging import math import textwrap -import functools from io import BytesIO from importlib import import_module -import xml.etree.ElementTree as ET from functools import wraps from functools import lru_cache
@@ -40,7 +37,6 @@ from gi.repository import Gdk from gi.repository import Gtk from gi.repository import GLib -from gi.repository import Gio from gi.repository import Pango from gi.repository import GdkPixbuf import nbxmpp @@ -48,7 +44,6 @@
from gajim.common import app from gajim.common import configpaths -from gajim.common import i18n from gajim.common.i18n import _ from gajim.common.helpers import URL_REGEX from gajim.common.const import LOCATION_DATA @@ -59,11 +54,13 @@
from .const import GajimIconSet from .const import WINDOW_MODULES +from .builder import Builder
_icon_theme = Gtk.IconTheme.get_default() if _icon_theme is not None: _icon_theme.append_search_path(str(configpaths.get('ICONS')))
+ log = logging.getLogger('gajim.gui.util')
@@ -150,55 +147,6 @@ def nick_matching(nick: str) -> bool: return matches + other_nicks
-class Builder: - def __init__(self, - filename: str, - widgets: List[str] = None, - domain: str = None, - gettext_: Any = None) -> None: - self._builder = Gtk.Builder() - - if domain is None: - domain = i18n.DOMAIN - self._builder.set_translation_domain(domain) - - if gettext_ is None: - gettext_ = _ - - xml_text = self._load_string_from_filename(filename, gettext_) - - if widgets is not None: - self._builder.add_objects_from_string(xml_text, widgets) - else: - self._builder.add_from_string(xml_text) - - @staticmethod - @functools.lru_cache(maxsize=None) - def _load_string_from_filename(filename, gettext_): - file_path = str(configpaths.get('GUI') / filename) - - if sys.platform == "win32": - # This is a workaround for non working translation on Windows - tree = ET.parse(file_path) - for node in tree.iter(): - if 'translatable' in node.attrib and node.text is not None: - node.text = gettext_(node.text) - - return ET.tostring(tree.getroot(), - encoding='unicode', - method='xml') - - file = Gio.File.new_for_path(file_path) - content = file.load_contents(None) - return content[1].decode() - - def __getattr__(self, name): - try: - return getattr(self._builder, name) - except AttributeError: - return self._builder.get_object(name) - - def get_builder(file_name: str, widgets: List[str] = None) -> Builder: return Builder(file_name, widgets)
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/2cb70610dda63a25b011b206da195758...