Daniel Brötzmann pushed to branch master at gajim / gajim
Commits: 8974f2ca by wurstsalat at 2022-03-10T23:27:33+01:00 EmojiData: Typing
- - - - - 6747e016 by wurstsalat at 2022-03-10T23:27:33+01:00 PlainWidget: Add missing emoji replace method
- - - - - 2d598a7b by wurstsalat at 2022-03-10T23:27:33+01:00 Remove obsolete HTMLTextView
- - - - -
3 changed files:
- gajim/gtk/conversation/plain_widget.py - gajim/gtk/emoji_data.py - − gajim/gtk/htmltextview.py
Changes:
===================================== gajim/gtk/conversation/plain_widget.py ===================================== @@ -21,6 +21,7 @@ from gi.repository import Gtk from gi.repository import Pango from gi.repository import Gdk +from gi.repository import GdkPixbuf from gi.repository import GLib from gi.repository import GObject
@@ -65,7 +66,7 @@ def add_content(self, block: PlainBlock) -> None: self._text_widget.print_text_with_styling(block)
def add_action_phrase(self, text: str, nickname: str) -> None: - text = text.replace('/me', '* %s' % nickname, 1) + text = text.replace('/me', f'* {nickname}', 1) text = GLib.markup_escape_text(text) self._text_widget.add_action_phrase(text)
@@ -255,6 +256,25 @@ def print_text_with_styling(self, block: PlainBlock) -> None: emoji_pixbufs.append_marks( self, start_mark, end_mark, emoji.text)
+ def replace_emoji(self, + start_mark: Gtk.TextMark, + end_mark: Gtk.TextMark, + pixbuf: GdkPixbuf.Pixbuf, + codepoint: str + ) -> None: + buffer_ = self.get_buffer() + start_iter = buffer_.get_iter_at_mark(start_mark) + end_iter = buffer_.get_iter_at_mark(end_mark) + buffer_.delete(start_iter, end_iter) + + anchor = buffer_.create_child_anchor(start_iter) + anchor.plaintext = codepoint # type: ignore + img = Gtk.Image.new_from_pixbuf(pixbuf) + img.show() + self.add_child_at_anchor(img, anchor) + buffer_.delete_mark(start_mark) + buffer_.delete_mark(end_mark) + def add_action_phrase(self, text: str) -> None: buffer_ = self.get_buffer() buffer_.insert(buffer_.get_start_iter(), text.strip()) @@ -268,7 +288,7 @@ def _query_tooltip(self, x_pos: int, y_pos: int, _keyboard_mode: bool, - tooltip: Gtk.Tooltip + _tooltip: Gtk.Tooltip ) -> bool:
window = widget.get_window(Gtk.TextWindowType.TEXT)
===================================== gajim/gtk/emoji_data.py ===================================== @@ -14,12 +14,15 @@
# pylint: disable=too-many-lines
+from typing import Optional + import re import weakref from enum import IntEnum from collections import OrderedDict
from gi.repository import GdkPixbuf +from gi.repository import Gtk
_MODIFIERS = ['\U0001F3FB', '\U0001F3FC', @@ -36,7 +39,7 @@ class Emoji(IntEnum): PARSE_COLUMNS = 20
-def is_emoji(codepoints): +def is_emoji(codepoints: str) -> bool: if codepoints in emoji_data: return True if codepoints in emoji_ascii_data: @@ -44,7 +47,7 @@ def is_emoji(codepoints): return False
-def get_emoji_pixbuf(codepoints): +def get_emoji_pixbuf(codepoints: str) -> Optional[GdkPixbuf.Pixbuf]: ascii_codepoint = emoji_ascii_data.get(codepoints, None) if ascii_codepoint is not None: codepoints = ascii_codepoint @@ -75,7 +78,7 @@ def __contains__(self, item): except KeyError: return False
- def get_regex(self): + def get_regex(self) -> str: emojis = [] for codepoint, attrs in self.items(): if attrs.get('variations', False): @@ -92,38 +95,49 @@ class EmojiPixbufs(dict):
def __init__(self): dict.__init__(self) - self._marks = weakref.WeakKeyDictionary() + self._marks: weakref.WeakKeyDictionary[ + Gtk.TextView, + list[tuple[ + weakref.ReferenceType[Gtk.TextMark], + weakref.ReferenceType[Gtk.TextMark], + str]] + ] = weakref.WeakKeyDictionary()
@property - def complete(self): + def complete(self) -> bool: return self._complete
@complete.setter - def complete(self, value): - if value is True: + def complete(self, value: bool) -> None: + if value: self._replace_emojis() self._marks.clear() self._complete = value
- def clear(self): + def clear(self) -> None: self.complete = False self._marks.clear() dict.clear(self)
- def append_marks(self, textview, start, end, codepoint): + def append_marks(self, + textview: Gtk.TextView, + start: Gtk.TextMark, + end: Gtk.TextMark, + codepoint: str + ) -> None: # We have to assign some dummy data to the mark, or else # pygobject will not keep the python wrapper alive, which in turn # makes the weakref invalid as soon as the method ends - start.dummy = 'x' - start = weakref.ref(start) - end.dummy = 'x' - end = weakref.ref(end) + start.dummy = 'x' # type: ignore + start_ref = weakref.ref(start) + end.dummy = 'x' # type: ignore + end_ref = weakref.ref(end) if textview in self._marks: - self._marks[textview].append((start, end, codepoint)) + self._marks[textview].append((start_ref, end_ref, codepoint)) else: - self._marks[textview] = [(start, end, codepoint)] + self._marks[textview] = [(start_ref, end_ref, codepoint)]
- def _replace_emojis(self): + def _replace_emojis(self) -> None: for textview, emojis in self._marks.items(): for emoji in emojis: start, end, codepoint = emoji @@ -140,7 +154,7 @@ def _replace_emojis(self): pixbuf = pixbuf.scale_simple(Emoji.TEXT_SIZE, Emoji.TEXT_SIZE, GdkPixbuf.InterpType.HYPER) - textview.replace_emojis( + textview.replace_emoji( start(), end(), pixbuf.copy(), codepoint)
@@ -148,7 +162,7 @@ def _replace_emojis(self):
# pylint: disable=line-too-long
-emoji_ascii_data = dict([ +emoji_ascii_data: dict[str, str] = dict([ ("':-D", '\U0001F605'), (':)', '\U0001F642'), (':-O', '\U0001F62e'),
===================================== gajim/gtk/htmltextview.py deleted ===================================== @@ -1,897 +0,0 @@ -# Copyright (C) 2005 Gustavo J. A. M. Carneiro -# Copyright (C) 2006 Santiago Gala -# Copyright (C) 2006-2007 Jean-Marie Traissard <jim AT lapin.org> -# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org> -# Copyright (C) 2007 Nikos Kouremenos <kourem AT gmail.com> -# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org> -# Julien Pivotto <roidelapluie AT gmail.com> -# Stephan Erb <steve-e AT h3c.de> -# -# 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/. - -""" -A Gtk.TextView-based renderer for XHTML-IM, as described in: - http://xmpp.org/extensions/xep-0071.html - -Starting with the version posted by Gustavo Carneiro, -I (Santiago Gala) am trying to make it more compatible -with the markup that docutils generate, and also more -modular. -""" - -import re -import logging -import xml.sax -import xml.sax.handler -from io import StringIO - -from gi.repository import GObject -from gi.repository import Pango -from gi.repository import Gtk -from gi.repository import Gdk -from gi.repository import GLib - -from gajim.common import app -from gajim.common.const import StyleAttr -from gajim.common.helpers import open_uri -from gajim.common.helpers import parse_uri - -from .menus import get_conv_context_menu -from .util import get_cursor - -log = logging.getLogger('gajim.htmlview') - -whitespace_rx = re.compile('\s+') -allwhitespace_rx = re.compile('^\s*$') - -# embryo of CSS classes -classes = { - #'system-message':';display: none', - 'problematic': ';color: red', -} - -# styles for elements -_element_styles = { - 'u' : ';text-decoration: underline', - 'em' : ';font-style: oblique', - 'cite' : '; background-color:rgb(170,190,250);' - 'font-style: oblique', - 'li' : '; margin-left: 1em; margin-right: 10%', - 'strong' : ';font-weight: bold', - 'pre' : '; background-color:rgb(190,190,190);' - 'font-family: monospace; white-space: pre;' - 'margin-left: 1em; margin-right: 10%', - 'kbd' : ';background-color:rgb(210,210,210);' - 'font-family: monospace', - 'blockquote' : '; background-color:rgb(170,190,250);' - 'margin-left: 2em; margin-right: 10%', - 'dt' : ';font-weight: bold; font-style: oblique', - 'dd' : ';margin-left: 2em; font-style: oblique' -} -# no difference for the moment -_element_styles['dfn'] = _element_styles['em'] -_element_styles['var'] = _element_styles['em'] -# deprecated, legacy, presentational -_element_styles['tt'] = _element_styles['kbd'] -_element_styles['i'] = _element_styles['em'] -_element_styles['b'] = _element_styles['strong'] - -_supported_style_attrs = [ - 'background-color', 'color', 'font-family', 'font-size', 'font-style', - 'font-weight', 'margin-left', 'margin-right', 'text-align', - 'text-decoration', 'white-space', 'display', 'width', 'height' -] - -# ========== -# XEP-0071 -# ========== -# -# This Integration Set includes a subset of the modules defined for -# XHTML 1.0 but does not redefine any existing modules, nor -# does it define any new modules. Specifically, it includes the -# following modules only: -# -# - Structure -# - Text -# -# * Block -# -# phrasal -# addr, blockquote, pre -# Struct -# div,p -# Heading -# h1, h2, h3, h4, h5, h6 -# -# * Inline -# -# phrasal -# abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var -# structural -# br, span -# -# - Hypertext (a) -# - List (ul, ol, dl) -# - Image (img) -# - Style Attribute -# -# Therefore XHTML-IM uses the following content models: -# -# Block.mix -# Block-like elements, e.g., paragraphs -# Flow.mix -# Any block or inline elements -# Inline.mix -# Character-level elements -# InlineNoAnchor.class -# Anchor element -# InlinePre.mix -# Pre element -# -# XHTML-IM also uses the following Attribute Groups: -# -# Core.extra.attrib -# TBD -# I18n.extra.attrib -# TBD -# Common.extra -# style -# -# -# ... -# block level: -# Heading h -# ( head = h1 | h2 | h3 | h4 | h5 | h6 ) -# Block ( phrasal = address | blockquote | pre ) -# NOT ( presentational = hr ) -# ( structural = div | p ) -# other: section -# Inline ( phrasal = abbr | acronym | cite | code | dfn | em | -# kbd | q | samp | strong | var ) -# NOT ( presentational = b | big | i | small | sub | sup | tt ) -# ( structural = br | span ) -# Param/Legacy param, font, basefont, center, s, strike, u, dir, menu, -# isindex - -BLOCK_HEAD = set(('h1', 'h2', 'h3', 'h4', 'h5', 'h6',)) -BLOCK_PHRASAL = set(('address', 'blockquote', 'pre',)) -BLOCK_PRES = set(('hr', )) #not in xhtml-im -BLOCK_STRUCT = set(('div', 'p', )) -BLOCK_HACKS = set(('table', 'tr')) # at the very least, they will start line ;) -BLOCK = BLOCK_HEAD | BLOCK_PHRASAL | BLOCK_STRUCT | BLOCK_PRES | BLOCK_HACKS - -INLINE_PHRASAL = set(['abbr', 'acronym', 'cite', 'code', 'dfn', 'em', - 'kbd', 'q', 'samp', 'strong', 'var']) -INLINE_PRES = set(['b', 'i', 'u', 'tt']) #not in xhtml-im -INLINE_STRUCT = set(['br', 'span']) -INLINE = INLINE_PHRASAL | INLINE_PRES | INLINE_STRUCT - -LIST_ELEMS = set(['dl', 'ol', 'ul']) - -for _name in BLOCK_HEAD: - _num = int(_name[1]) - _header_size = (_num - 1) // 2 - _weight = (_num - 1) % 2 - _element_styles[_name] = '; font-size: %s; %s' % ( - ('large', 'medium', 'small')[_header_size], - ('font-weight: bold', 'font-style: oblique')[_weight]) - -def _parse_css_color(color): - rgba = Gdk.RGBA() - success = rgba.parse(color) - if not success: - log.warning('Can't parse color: %s', color) - return rgba - -def style_iter(style): - for item in style.split(';'): - if item.strip(): - yield [x.strip() for x in item.split(':', 1)] - - -class HtmlHandler(xml.sax.handler.ContentHandler): - """ - A handler to display html to a gtk textview - - It keeps a stack of "style spans" (start/end element pairs) and a stack of - list counters, for nested lists. - """ - def __init__(self, textview, startiter): - xml.sax.handler.ContentHandler.__init__(self) - self.textbuf = textview.get_buffer() - self.textview = textview - self.iter = startiter - self.text = '' - self.starting = True - self.preserve = False - self.styles = [] # a Gtk.TextTag or None, for each span level - self.list_counters = [] # stack (top at head) of list - # counters, or None for unordered list - - # build a dictionary mapping styles to methods - self.__style_methods = {} - for style in _supported_style_attrs: - method_names = '_parse_style_%s' % style.replace('-', '_') - self.__style_methods[style] = method_names - - def _get_points_from_pixels(self, pixels): - resolution = self.textview.get_screen().get_resolution() - # points = pixels * 72 / resolution - return pixels * 72 / resolution - - @staticmethod - def _parse_style_color(tag, value): - color = _parse_css_color(value) - tag.set_property('foreground-rgba', color) - - @staticmethod - def _parse_style_background_color(tag, value): - color = _parse_css_color(value) - tag.set_property('background-rgba', color) - tag.set_property('paragraph-background-rgba', color) - - @staticmethod - def __parse_length_frac_size_allocate(_textview, allocation, frac, - callback, args): - callback(allocation.width*frac, *args) - - def _parse_length(self, value, font_relative, block_relative, minl, maxl, - callback, *args): - """ - Parse/calc length, converting to pixels, calls callback(length, *args) - when the length is first computed or changes - """ - if value.endswith('%'): - val = float(value[:-1]) - if val > 0: - sign = 1 - elif val < 0: - sign = -1 - else: - sign = 0 - # limits: 1% to 500% - val = sign*max(1, min(abs(val), 500)) - frac = val/100 - if font_relative: - callback(frac, '%', *args) - elif block_relative: - # CSS says 'Percentage values: refer to width of the closest - # block-level ancestor' - # This is difficult/impossible to implement, so we use - # textview width instead; a reasonable approximation.. - alloc = self.textview.get_allocation() - self.__parse_length_frac_size_allocate( - self.textview, alloc, frac, callback, args) - self.textview.connect('size-allocate', - self.__parse_length_frac_size_allocate, - frac, callback, args) - else: - callback(frac, *args) - return - - def get_val(min_val=minl, max_val=maxl): - try: - val = float(value[:-2]) - except Exception: - log.warning('Unable to parse length value "%s"', value) - return None - if val > 0: - sign = 1 - elif val < 0: - sign = -1 - else: - sign = 0 - # validate length - return sign*max(min_val, min(abs(val), max_val)) - if value.endswith('pt'): # points - size = get_val(5, 50) - if size is None: - return - callback(size, 'pt', *args) - - elif value.endswith('em'): - size = get_val(0.3, 4) - if size is None: - return - callback(size, 'em', *args) - - elif value.endswith('px'): # pixels - size = get_val(5, 50) - if size is None: - return - callback(size, 'px', *args) - - else: - try: - # TODO: isn't "no units" interpreted as pixels? - val = int(value) - if val > 0: - sign = 1 - elif val < 0: - sign = -1 - else: - sign = 0 - # validate length - val = sign*max(5, min(abs(val), 70)) - callback(val, 'px', *args) - except Exception: - log.warning('Unable to parse length value "%s"', value) - - def __parse_font_size_cb(self, size, type_, tag): - if type_ in ('em', '%'): - tag.set_property('scale', size) - elif type_ == 'pt': - tag.set_property('size-points', size) - elif type_ == 'px': - tag.set_property('size-points', self._get_points_from_pixels(size)) - - @staticmethod - def _parse_style_display(tag, value): - if value == 'none': - tag.set_property('invisible', 'true') - # FIXME: display: block, inline - - def _parse_style_font_size(self, tag, value): - try: - scale = { - 'xx-small': 0.5787037037037, - 'x-small': 0.6444444444444, - 'small': 0.8333333333333, - 'medium': 1.0, - 'large': 1.2, - 'x-large': 1.4399999999999, - 'xx-large': 1.728, - }[value] - except KeyError: - pass - else: - tag.set_property('scale', scale) - return - if value == 'smaller': - tag.set_property('scale', 0.8333333333333) - return - if value == 'larger': - tag.set_property('scale', 1.2) - return - # font relative (5 ~ 4pt, 110 ~ 72pt) - self._parse_length( - value, True, False, 5, 110, self.__parse_font_size_cb, tag) - - @staticmethod - def _parse_style_font_style(tag, value): - try: - style = { - 'normal': Pango.Style.NORMAL, - 'italic': Pango.Style.ITALIC, - 'oblique': Pango.Style.OBLIQUE, - }[value] - except KeyError: - log.warning('unknown font-style %s', value) - else: - tag.set_property('style', style) - - def __frac_length_tag_cb(self, length, tag, propname): - styles = self._get_style_tags() - if styles: - length += styles[-1].get_property(propname) - tag.set_property(propname, length) - - def _parse_style_margin_left(self, tag, value): - # block relative - self._parse_length(value, False, True, 1, 1000, - self.__frac_length_tag_cb, tag, 'left-margin') - - def _parse_style_margin_right(self, tag, value): - # block relative - self._parse_length(value, False, True, 1, 1000, - self.__frac_length_tag_cb, tag, 'right-margin') - - @staticmethod - def _parse_style_font_weight(tag, value): - # TODO: missing 'bolder' and 'lighter' - try: - weight = { - '100': Pango.Weight.ULTRALIGHT, - '200': Pango.Weight.ULTRALIGHT, - '300': Pango.Weight.LIGHT, - '400': Pango.Weight.NORMAL, - '500': Pango.Weight.NORMAL, - '600': Pango.Weight.BOLD, - '700': Pango.Weight.BOLD, - '800': Pango.Weight.ULTRABOLD, - '900': Pango.Weight.HEAVY, - 'normal': Pango.Weight.NORMAL, - 'bold': Pango.Weight.BOLD, - }[value] - except KeyError: - log.warning('unknown font-style %s', value) - else: - tag.set_property('weight', weight) - - @staticmethod - def _parse_style_font_family(tag, value): - tag.set_property('family', value) - - @staticmethod - def _parse_style_text_align(tag, value): - try: - align = { - 'left': Gtk.Justification.LEFT, - 'right': Gtk.Justification.RIGHT, - 'center': Gtk.Justification.CENTER, - 'justify': Gtk.Justification.FILL, - }[value] - except KeyError: - log.warning('Invalid text-align: %s requested', value) - else: - tag.set_property('justification', align) - - @staticmethod - def _parse_style_text_decoration(tag, value): - values = value.split(' ') - if 'none' in values: - tag.set_property('underline', Pango.Underline.NONE) - tag.set_property('strikethrough', False) - if 'underline' in values: - tag.set_property('underline', Pango.Underline.SINGLE) - else: - tag.set_property('underline', Pango.Underline.NONE) - if 'line-through' in values: - tag.set_property('strikethrough', True) - else: - tag.set_property('strikethrough', False) - if 'blink' in values: - log.warning('text-decoration:blink not implemented') - if 'overline' in values: - log.warning('text-decoration:overline not implemented') - - @staticmethod - def _parse_style_white_space(tag, value): - if value == 'pre': - tag.set_property('wrap_mode', Gtk.WrapMode.NONE) - elif value == 'normal': - tag.set_property('wrap_mode', Gtk.WrapMode.WORD) - elif value == 'nowrap': - tag.set_property('wrap_mode', Gtk.WrapMode.NONE) - - @staticmethod - def __length_tag_cb(value, tag, propname): - try: - tag.set_property(propname, value) - except Exception: - log.warning('Error with prop: %s for tag: %s', propname, str(tag)) - - def _parse_style_width(self, tag, value): - if value == 'auto': - return - self._parse_length(value, False, False, 1, 1000, - self.__length_tag_cb, tag, "width") - def _parse_style_height(self, tag, value): - if value == 'auto': - return - self._parse_length(value, False, False, 1, 1000, - self.__length_tag_cb, tag, "height") - - def _get_style_tags(self): - return [tag for tag in self.styles if tag is not None] - - def _create_url(self, href, title, type_, id_): - '''Process a url tag. - ''' - tag = self.textbuf.create_tag(id_) - if href and href[0] != '#': - tag.href = href - tag.type_ = type_ # to be used by the URL handler - tag.connect('event', self.textview.hyperlink_handler, 'url') - tag.set_property('foreground', - app.css_config.get_value('.gajim-url', - StyleAttr.COLOR)) - tag.set_property('underline', Pango.Underline.SINGLE) - tag.is_anchor = True - if title: - tag.title = title - return tag - - def _begin_span(self, style, tag=None, id_=None): - if style is None: - self.styles.append(tag) - return - if tag is None: - if id_: - tag = self.textbuf.create_tag(id_) - else: - tag = self.textbuf.create_tag() # we create anonymous tag - for attr, val in style_iter(style): - attr = attr.lower() - try: - getattr(self, self.__style_methods[attr])(tag, val) - except KeyError: - log.warning('Style attribute "%s" requested ' - 'but not yet implemented', attr) - self.styles.append(tag) - - def _end_span(self): - self.styles.pop() - - def _jump_line(self): - self.textbuf.insert_with_tags_by_name(self.iter, '\n', 'eol') - self.starting = True - - def _insert_text(self, text, working_iter=None): - if working_iter is None: - working_iter = self.iter - if self.starting and text != '\n': - self.starting = (text[-1] == '\n') - tags = self._get_style_tags() - if tags: - self.textbuf.insert_with_tags(working_iter, text, *tags) - else: - self.textbuf.insert(working_iter, text) - - def _starts_line(self): - return self.starting or self.iter.starts_line() - - def _flush_text(self): - if not self.text: - return - text, self.text = self.text, '' - if not self.preserve: - text = text.replace('\n', ' ') - self.handle_specials(whitespace_rx.sub(' ', text)) - else: - self._insert_text(text.strip('\n')) - - def _anchor_event(self, _tag, _textview, event, _iter, href, type_): - if event.type == Gdk.EventType.BUTTON_PRESS: - self.textview.emit('url-clicked', href, type_) - return True - return False - - def handle_specials(self, text): - # TODO: Parse formatting - # self.iter = self.textview.parse_formatting( - # text, self._get_style_tags(), iter_=self.iter) - self._insert_text(text) - - def characters(self, content): - if self.preserve: - self.text += content - return - if allwhitespace_rx.match(content) is not None and self._starts_line(): - return - self.text += content - self.starting = False - - def startElement(self, name, attrs): - self._flush_text() - klass = [i for i in attrs.get('class', ' ').split(' ') if i] - style = '' - #Add styles defined for classes - for k in klass: - if k in classes: - style += classes[k] - - tag = None - #FIXME: if we want to use id, it needs to be unique across - # the whole textview, so we need to add something like the - # message-id to it. - #id_ = attrs.get('id',None) - id_ = None - if name == 'a': - #TODO: accesskey, charset, hreflang, rel, rev, tabindex, type - href = attrs.get('href', None) - if not href: - href = attrs.get('HREF', None) - # Gaim sends HREF instead of href - title = attrs.get('title', attrs.get('rel', href)) - type_ = attrs.get('type', None) - tag = self._create_url(href, title, type_, id_) - elif name == 'blockquote': - cite = attrs.get('cite', None) - if cite: - tag = self.textbuf.create_tag(id_) - tag.title = attrs.get('title', None) - tag.is_anchor = True - elif name in LIST_ELEMS: - style += ';margin-left: 2em' - - if name in _element_styles: - style += _element_styles[name] - # so that explicit styles override implicit ones, - # we add the attribute last - style += ";"+attrs.get('style', '') - if style == '': - style = None - self._begin_span(style, tag, id_) - - if name == 'br': - pass # handled in endElement - elif name == 'hr': - pass # handled in endElement - elif name in BLOCK: - if not self._starts_line(): - self._jump_line() - if name == 'pre': - self.preserve = True - elif name == 'span': - pass - elif name in ('dl', 'ul'): - if not self._starts_line(): - self._jump_line() - self.list_counters.append(None) - elif name == 'ol': - if not self._starts_line(): - self._jump_line() - self.list_counters.append(0) - elif name == 'li': - if self.list_counters[-1] is None: - li_head = chr(0x2022) - else: - self.list_counters[-1] += 1 - li_head = '%i.' % self.list_counters[-1] - self.text = ' '*len(self.list_counters)*4 + li_head + ' ' - self._flush_text() - self.starting = True - elif name == 'dd': - self._jump_line() - elif name == 'dt': - if not self.starting: - self._jump_line() - elif name in ('a', 'img', 'body', 'html'): - pass - elif name in INLINE: - pass - else: - log.warning('Unhandled element "%s"', name) - - def endElement(self, name): - end_preserving = False - newline = False - if name == 'br': - newline = True - elif name == 'hr': - #FIXME: plenty of unused attributes (width, height,...) :) - self._jump_line() - self._insert_text('\u2015'*40) - self._jump_line() - elif name in LIST_ELEMS: - self.list_counters.pop() - elif name == 'li': - newline = True - elif name == 'img': - pass - elif name in ('body', 'html'): - pass - elif name == 'a': - pass - elif name in INLINE: - pass - elif name in ('dd', 'dt', ): - pass - elif name in BLOCK: - if name == 'pre': - end_preserving = True - elif name in BLOCK_STRUCT: - newline = True - else: - log.warning("Unhandled element '%s'", name) - self._flush_text() - if end_preserving: - self.preserve = False - if newline: - self._jump_line() - self._end_span() - - -class HtmlTextView(Gtk.TextView): - - _tags = ['url', 'mail', 'xmpp', 'sth_at_sth'] - - def __init__(self, account, standalone=False): - Gtk.TextView.__init__(self) - - self.set_has_tooltip(True) - self.set_border_width(1) - self.set_accepts_tab(True) - self.set_editable(False) - self.set_cursor_visible(False) - self.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - self.set_left_margin(2) - self.set_right_margin(2) - - self.drag_dest_unset() - - self.connect('copy-clipboard', self._on_copy_clipboard) - self.connect('destroy', self._on_destroy) - self.get_buffer().eol_tag = self.get_buffer().create_tag('eol') - - self.account = account - self.plugin_modified = False - self._cursor_changed = False - - if standalone: - self.connect('query-tooltip', self._query_tooltip) - self.create_tags() - - def _on_destroy(self, *args): - # We restore the TextView’s drag destination to avoid a GTK warning - # when closing the control. BaseControl.shutdown() calls destroy() - # on the control’s main box, causing GTK to recursively destroy the - # child widgets. GTK then tries to set a target list on the TextView, - # resulting in a warning because the Widget has no drag destination. - self.drag_dest_set( - Gtk.DestDefaults.ALL, - None, - Gdk.DragAction.DEFAULT) - - def create_tags(self): - color = app.css_config.get_value('.gajim-url', StyleAttr.COLOR) - - attrs = {'foreground': color, - 'underline': Pango.Underline.SINGLE} - - for tag in self._tags: - tag_ = self.get_buffer().create_tag(tag, **attrs) - tag_.connect('event', self.hyperlink_handler, tag) - - def update_tags(self): - tag_table = self.get_buffer().get_tag_table() - color = app.css_config.get_value('.gajim-url', StyleAttr.COLOR) - - for tag in self._tags: - tag_table.lookup(tag).set_property('foreground', color) - - def _query_tooltip(self, widget, x_pos, y_pos, _keyboard_mode, tooltip): - window = widget.get_window(Gtk.TextWindowType.TEXT) - x_pos, y_pos = self.window_to_buffer_coords( - Gtk.TextWindowType.TEXT, x_pos, y_pos) - iter_ = self.get_iter_at_position(x_pos, y_pos)[1] - - for tag in iter_.get_tags(): - if getattr(tag, 'is_anchor', False): - text = getattr(tag, 'title', False) - if text: - if len(text) > 50: - text = text[:47] + '…' - tooltip.set_text(text) - window.set_cursor(get_cursor('pointer')) - self._cursor_changed = True - return True - - tag_name = tag.get_property('name') - if tag_name in ('url', 'mail', 'xmpp', 'sth_at_sth'): - window.set_cursor(get_cursor('pointer')) - self._cursor_changed = True - return False - - if self._cursor_changed: - window.set_cursor(get_cursor('text')) - self._cursor_changed = False - return False - - def show_context_menu(self, uri): - menu = get_conv_context_menu(self.account, uri) - if menu is None: - log.warning('No handler for URI type: %s', uri) - return - - def destroy(menu, _pspec): - visible = menu.get_property('visible') - if not visible: - GLib.idle_add(menu.destroy) - - menu.attach_to_widget(self, None) - menu.connect('notify::visible', destroy) - menu.popup_at_pointer() - - def hyperlink_handler(self, texttag, _widget, event, iter_, _kind): - if event.type != Gdk.EventType.BUTTON_PRESS: - return Gdk.EVENT_PROPAGATE - - begin_iter = iter_.copy() - # we get the beginning of the tag - while not begin_iter.starts_tag(texttag): - begin_iter.backward_char() - end_iter = iter_.copy() - # we get the end of the tag - while not end_iter.ends_tag(texttag): - end_iter.forward_char() - - # Detect XHTML-IM link - word = getattr(texttag, 'href', None) - if not word: - word = self.get_buffer().get_text(begin_iter, end_iter, True) - - uri = parse_uri(word) - if event.button.button == 3: # right click - self.show_context_menu(uri) - return Gdk.EVENT_STOP - - self.plugin_modified = False - app.plugin_manager.extension_point( - 'hyperlink_handler', uri, self, self.get_toplevel()) - if self.plugin_modified: - return Gdk.EVENT_STOP - - open_uri(uri, account=self.account) - return Gdk.EVENT_STOP - - def display_html(self, html, textview, iter_=None): - buffer_ = self.get_buffer() - if iter_: - eob = iter_ - else: - eob = buffer_.get_end_iter() - parser = xml.sax.make_parser() - parser.setContentHandler(HtmlHandler(textview, eob)) - parser.parse(StringIO(html)) - # If the xhtml ends with a BLOCK element we have to remove - # the \n we add after BLOCK elements - self._delete_last_char(buffer_, eob) - - @staticmethod - def _delete_last_char(buffer_, iter_): - start_iter = iter_.copy() - start_iter.backward_char() - text = buffer_.get_text(start_iter, iter_, True) - if text == '\n': - buffer_.delete(start_iter, iter_) - - @staticmethod - def _on_copy_clipboard(textview): - clipboard = textview.get_clipboard(Gdk.SELECTION_CLIPBOARD) - selected = textview.get_selected_text() - clipboard.set_text(selected, -1) - GObject.signal_stop_emission_by_name(textview, 'copy-clipboard') - - def get_selected_text(self): - bounds = self.get_buffer().get_selection_bounds() - selection = '' - if bounds: - (search_iter, end) = bounds - - while search_iter.compare(end): - character = search_iter.get_char() - if character == '\ufffc': - anchor = search_iter.get_child_anchor() - if anchor: - text = anchor.plaintext - if text: - selection += text - else: - selection += character - else: - selection += character - search_iter.forward_char() - return selection - - def replace_emojis(self, start_mark, end_mark, pixbuf, codepoint): - buffer_ = self.get_buffer() - start_iter = buffer_.get_iter_at_mark(start_mark) - end_iter = buffer_.get_iter_at_mark(end_mark) - buffer_.delete(start_iter, end_iter) - - anchor = buffer_.create_child_anchor(start_iter) - anchor.plaintext = codepoint - emoji = Gtk.Image.new_from_pixbuf(pixbuf) - emoji.show() - self.add_child_at_anchor(emoji, anchor) - buffer_.delete_mark(start_mark) - buffer_.delete_mark(end_mark) - - def unselect(self): - buffer_ = self.get_buffer() - insert_iter = buffer_.get_iter_at_mark(buffer_.get_insert()) - buffer_.select_range(insert_iter, insert_iter)
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/a9c5691a5c83775a997e55803db84306...