Daniel Brötzmann pushed to branch master at gajim / gajim
Commits: 165dda84 by wurstsalat at 2021-12-13T20:44:58+01:00 MessageInputTextView: Remove XHTML formatting actions, add markup actions
- - - - - e9c0416a by wurstsalat at 2021-12-13T20:53:17+01:00 MessageInputTextView: Remove XHTML code around sending XHTML
- - - - - 14704ff1 by wurstsalat at 2021-12-13T21:22:38+01:00 MessageInputTextView: Add real-time markup parsing
- - - - -
6 changed files:
- gajim/data/gui/chat_control.ui - gajim/data/gui/groupchat_control.ui - gajim/gtk/controls/base.py - gajim/gtk/controls/chat.py - gajim/gtk/controls/groupchat.py - gajim/gtk/message_input.py
Changes:
===================================== gajim/data/gui/chat_control.ui ===================================== @@ -51,75 +51,30 @@ <property name="visible">True</property> <property name="can-focus">False</property> <child> - <object class="GtkCheckMenuItem" id="bold"> + <object class="GtkMenuItem" id="bold"> <property name="name">bold</property> <property name="visible">True</property> <property name="can-focus">False</property> <property name="label" translatable="yes">Bold</property> - <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/> + <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/> </object> </child> <child> - <object class="GtkCheckMenuItem" id="italic"> + <object class="GtkMenuItem" id="italic"> <property name="name">italic</property> <property name="visible">True</property> <property name="can-focus">False</property> <property name="label" translatable="yes">Italic</property> - <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/> + <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/> </object> </child> <child> - <object class="GtkCheckMenuItem" id="underline"> - <property name="name">underline</property> - <property name="visible">True</property> - <property name="can-focus">False</property> - <property name="label" translatable="yes">Underline</property> - <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/> - </object> - </child> - <child> - <object class="GtkCheckMenuItem" id="strike"> + <object class="GtkMenuItem" id="strike"> <property name="name">strike</property> <property name="visible">True</property> <property name="can-focus">False</property> <property name="label" translatable="yes">Strike</property> - <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/> - </object> - </child> - <child> - <object class="GtkSeparatorMenuItem"> - <property name="visible">True</property> - <property name="can-focus">False</property> - </object> - </child> - <child> - <object class="GtkMenuItem" id="color"> - <property name="visible">True</property> - <property name="can-focus">False</property> - <property name="label" translatable="yes">Color</property> - <signal name="activate" handler="on_color_menuitem_activate" swapped="no"/> - </object> - </child> - <child> - <object class="GtkMenuItem" id="font"> - <property name="visible">True</property> - <property name="can-focus">False</property> - <property name="label" translatable="yes">Font</property> - <signal name="activate" handler="on_font_menuitem_activate" swapped="no"/> - </object> - </child> - <child> - <object class="GtkSeparatorMenuItem"> - <property name="visible">True</property> - <property name="can-focus">False</property> - </object> - </child> - <child> - <object class="GtkMenuItem" id="clear_formatting"> - <property name="visible">True</property> - <property name="can-focus">False</property> - <property name="label" translatable="yes">Clear formatting</property> - <signal name="activate" handler="on_clear_formatting_menuitem_activate" swapped="no"/> + <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/> </object> </child> </object> @@ -400,12 +355,12 @@ <property name="can-focus">True</property> <property name="focus-on-click">False</property> <property name="receives-default">True</property> - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <property name="has-tooltip">True</property> + <property name="tooltip-text" translatable="yes">Format your message…</property> <property name="relief">none</property> <property name="popup">formattings_menu</property> + <property name="direction">up</property> <child> - <object class="GtkImage" id="image10"> + <object class="GtkImage"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="icon-name">format-text-bold-symbolic</property>
===================================== gajim/data/gui/groupchat_control.ui ===================================== @@ -51,75 +51,30 @@ <property name="visible">True</property> <property name="can-focus">False</property> <child> - <object class="GtkCheckMenuItem" id="bold"> + <object class="GtkMenuItem" id="bold"> <property name="name">bold</property> <property name="visible">True</property> <property name="can-focus">False</property> <property name="label" translatable="yes">Bold</property> - <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/> + <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/> </object> </child> <child> - <object class="GtkCheckMenuItem" id="italic"> + <object class="GtkMenuItem" id="italic"> <property name="name">italic</property> <property name="visible">True</property> <property name="can-focus">False</property> <property name="label" translatable="yes">Italic</property> - <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/> + <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/> </object> </child> <child> - <object class="GtkCheckMenuItem" id="underline"> - <property name="name">underline</property> - <property name="visible">True</property> - <property name="can-focus">False</property> - <property name="label" translatable="yes">Underline</property> - <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/> - </object> - </child> - <child> - <object class="GtkCheckMenuItem" id="strike"> + <object class="GtkMenuItem" id="strike"> <property name="name">strike</property> <property name="visible">True</property> <property name="can-focus">False</property> <property name="label" translatable="yes">Strike</property> - <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/> - </object> - </child> - <child> - <object class="GtkSeparatorMenuItem"> - <property name="visible">True</property> - <property name="can-focus">False</property> - </object> - </child> - <child> - <object class="GtkMenuItem" id="color"> - <property name="visible">True</property> - <property name="can-focus">False</property> - <property name="label" translatable="yes">Color</property> - <signal name="activate" handler="on_color_menuitem_activate" swapped="no"/> - </object> - </child> - <child> - <object class="GtkMenuItem" id="font"> - <property name="visible">True</property> - <property name="can-focus">False</property> - <property name="label" translatable="yes">Font</property> - <signal name="activate" handler="on_font_menuitem_activate" swapped="no"/> - </object> - </child> - <child> - <object class="GtkSeparatorMenuItem"> - <property name="visible">True</property> - <property name="can-focus">False</property> - </object> - </child> - <child> - <object class="GtkMenuItem" id="clear_formatting"> - <property name="visible">True</property> - <property name="can-focus">False</property> - <property name="label" translatable="yes">Clear formatting</property> - <signal name="activate" handler="on_clear_formatting_menuitem_activate" swapped="no"/> + <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/> </object> </child> </object> @@ -584,14 +539,13 @@ <child> <object class="GtkMenuButton" id="formattings_button"> <property name="visible">True</property> - <property name="sensitive">False</property> <property name="can-focus">True</property> <property name="focus-on-click">False</property> <property name="receives-default">True</property> - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <property name="has-tooltip">True</property> + <property name="tooltip-text" translatable="yes">Format your message</property> <property name="relief">none</property> <property name="popup">formattings_menu</property> + <property name="direction">up</property> <child> <object class="GtkImage"> <property name="visible">True</property>
===================================== gajim/gtk/controls/base.py ===================================== @@ -1024,14 +1024,12 @@ def get_our_nick(self) -> str: def _on_send_message(self, *args): self.msg_textview.replace_emojis() message = self.msg_textview.get_text() - xhtml = self.msg_textview.get_xhtml() - self.send_message(message, xhtml=xhtml) + self.send_message(message)
def send_message(self, message: str, type_: str = 'chat', resource: Optional[str] = None, - xhtml: Optional[str] = None, process_commands: bool = True, attention: bool = False ) -> None: @@ -1065,8 +1063,7 @@ def send_message(self, label=label, control=self, attention=attention, - correct_id=correct_id, - xhtml=xhtml) + correct_id=correct_id)
self._client.send_message(message_)
@@ -1076,9 +1073,7 @@ def send_message(self, # Be sure to send user nickname only once according to JEP-0172 self.user_nick = None
- # Clear msg input - message_buffer = self.msg_textview.get_buffer() - message_buffer.set_text('') # clear message buffer (and tv of course) + self.msg_textview.clear()
def _on_message_tv_buffer_changed(self, textbuffer): has_text = self.msg_textview.has_text() @@ -1303,25 +1298,11 @@ def _on_emoticon_button_clicked(self, _widget): self.msg_textview.emit('insert-emoji') self.xml.emoticons_button.set_property('active', False)
- def on_color_menuitem_activate(self, _widget): - color_dialog = Gtk.ColorChooserDialog(None, app.window) - color_dialog.set_use_alpha(False) - color_dialog.connect('response', self.msg_textview.color_set) - color_dialog.show_all() - - def on_font_menuitem_activate(self, _widget): - font_dialog = Gtk.FontChooserDialog(None, app.window) - start, finish = self.msg_textview.get_active_iters() - font_dialog.connect( - 'response', self.msg_textview.font_set, start, finish) - font_dialog.show_all() - - def on_formatting_menuitem_activate(self, widget): - tag = widget.get_name() - self.msg_textview.set_tag(tag) - - def on_clear_formatting_menuitem_activate(self, _widget): - self.msg_textview.clear_tags() + def _on_formatting_menuitem_activate(self, + menu_item: Gtk.CheckMenuItem + ) -> None: + formatting = menu_item.get_name() + self.msg_textview.apply_formatting(formatting)
def _style_changed(self, *args): self.update_text_tags()
===================================== gajim/gtk/controls/chat.py ===================================== @@ -307,18 +307,6 @@ def _on_send_marker(self, action, param): action.set_state(param) self.contact.settings.set('send_marker', param.get_boolean())
- def _update_toolbar(self) -> None: - # Formatting - # TODO: find out what encryption allows for xhtml and which not - if self.contact.supports(Namespace.XHTML_IM): - self.xml.formattings_button.set_sensitive(True) - self.xml.formattings_button.set_tooltip_text(_( - 'Show a list of formattings')) - else: - self.xml.formattings_button.set_sensitive(False) - self.xml.formattings_button.set_tooltip_text( - _('This contact does not support HTML')) - def update_all_pep_types(self) -> None: self._update_pep(PEPEventType.LOCATION) self._update_pep(PEPEventType.TUNE)
===================================== gajim/gtk/controls/groupchat.py ===================================== @@ -883,9 +883,6 @@ def _set_control_active(self) -> None: self.msg_textview.set_editable(True)
self.roster.initial_draw() - - self.xml.formattings_button.set_sensitive(True) - self.conversation_view.update_avatars()
self.update_actions()
===================================== gajim/gtk/message_input.py ===================================== @@ -17,7 +17,6 @@ # 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 Optional from typing import Dict from typing import List from typing import Tuple @@ -27,12 +26,9 @@ from gi.repository import GLib from gi.repository import Pango
-from nbxmpp.modules.misc import build_xhtml_body - from gajim.common import app -from gajim.common.regex import LINK_REGEX +from gajim.common.styling import process
-from .util import convert_rgba_to_hex from .util import scroll_to_end
if app.is_installed('GSPELL'): @@ -40,14 +36,20 @@
UNDO_LIMIT: int = 20
+FORMAT_CHARS: Dict[str, str] = { + 'bold': '*', + 'italic': '_', + 'strike': '~', + 'pre': '`', +} +
class MessageInputTextView(Gtk.TextView): """ A Gtk.Textview for chat message input """ - def __init__(self): + def __init__(self) -> None: Gtk.TextView.__init__(self) - self.set_border_width(3) self.set_accepts_tab(True) self.set_editable(True) @@ -61,36 +63,15 @@ def __init__(self):
self.drag_dest_unset()
- self.undo_list: List[str] = [] + self._undo_list: List[str] = [] self.undo_pressed: bool = False
- self.begin_tags: Dict[str, str] = {} - self.end_tags: Dict[str, str] = {} - self.color_tags: List[str] = [] - self.fonts_tags: List[str] = [] - self.other_tags: Dict[str, Gtk.TextTag] = {} - - buffer_ = self.get_buffer() - self.other_tags['bold'] = buffer_.create_tag('bold') - self.other_tags['bold'].set_property('weight', Pango.Weight.BOLD) - self.begin_tags['bold'] = '<strong>' - self.end_tags['bold'] = '</strong>' - self.other_tags['italic'] = buffer_.create_tag('italic') - self.other_tags['italic'].set_property('style', Pango.Style.ITALIC) - self.begin_tags['italic'] = '<em>' - self.end_tags['italic'] = '</em>' - self.other_tags['underline'] = buffer_.create_tag('underline') - self.other_tags['underline'].set_property( - 'underline', Pango.Underline.SINGLE) - underline = '<span style="text-decoration: underline;">' - self.begin_tags['underline'] = underline - self.end_tags['underline'] = '</span>' - self.other_tags['strike'] = buffer_.create_tag('strike') - self.other_tags['strike'].set_property('strikethrough', True) - strike = '<span style="text-decoration: line-through;">' - self.begin_tags['strike'] = strike - self.end_tags['strike'] = '</span>' + self.get_buffer().create_tag('strong', weight=Pango.Weight.BOLD) + self.get_buffer().create_tag('emphasis', style=Pango.Style.ITALIC) + self.get_buffer().create_tag('strike', strikethrough=True) + self.get_buffer().create_tag('pre', family='monospace')
+ self.get_buffer().connect('changed', self._on_text_changed) self.connect_after('paste-clipboard', self._after_paste_clipboard) self.connect('focus-in-event', self._on_focus_in) self.connect('focus-out-event', self._on_focus_out) @@ -120,6 +101,18 @@ def _on_focus_out(self, _widget, _event): self.toggle_speller(False) return False
+ def _on_text_changed(self, buffer_): + text = self.get_text() + if not text: + return + + result = process(text) + for block in result.blocks: + for span in block.spans: + start_iter = buffer_.get_iter_at_offset(span.start) + end_iter = buffer_.get_iter_at_offset(span.end) + buffer_.apply_tag_by_name(span.name, start_iter, end_iter) + def insert_text(self, text: str) -> None: self.get_buffer().insert_at_cursor(text)
@@ -149,229 +142,86 @@ def toggle_speller(self, activate: bool) -> None: spell_view.set_inline_spell_checking(activate)
@staticmethod - def _after_paste_clipboard(textview): + def _after_paste_clipboard(textview: Gtk.TextView) -> None: buffer_ = textview.get_buffer() mark = buffer_.get_insert() iter_ = buffer_.get_iter_at_mark(mark) if iter_.get_offset() == buffer_.get_end_iter().get_offset(): GLib.idle_add(scroll_to_end, textview.get_parent())
- def make_clickable_urls(self, text: str) -> str: - _buffer = self.get_buffer() - - start = 0 - end = 0 - index = 0 - - new_text = '' - iterator = LINK_REGEX.finditer(text) - for match in iterator: - start, end = match.span() - url = text[start:end] - if start != 0: - text_before_special_text = text[index:start] - else: - text_before_special_text = '' - # we insert normal text - new_text += f'{text_before_special_text}<a href="{url}">{url}</a>' - index = end # update index - - if end < len(text): - new_text += text[end:] - - return new_text # the position after *last* special text - - def get_active_tags(self) -> List[str]: - start = self.get_active_iters()[0] - active_tags = [] - for tag in start.get_tags(): - active_tags.append(tag.get_property('name')) - return active_tags - - def get_active_iters(self) -> Tuple[Gtk.TextIter, Gtk.TextIter]: + def _get_active_iters(self) -> Tuple[Gtk.TextIter, Gtk.TextIter]: _buffer = self.get_buffer() return_val = _buffer.get_selection_bounds() - if return_val: # if sth was selected - start, finish = return_val[0], return_val[1] + if return_val: # if something is selected + start, end = return_val[0], return_val[1] else: - start, finish = _buffer.get_bounds() - return (start, finish) + start, end = _buffer.get_bounds() + return (start, end)
- def set_tag(self, tag: str) -> None: - _buffer = self.get_buffer() - start, finish = self.get_active_iters() - if start.has_tag(self.other_tags[tag]): - _buffer.remove_tag_by_name(tag, start, finish) - else: - if tag == 'underline': - _buffer.remove_tag_by_name('strike', start, finish) - elif tag == 'strike': - _buffer.remove_tag_by_name('underline', start, finish) - _buffer.apply_tag_by_name(tag, start, finish) + def apply_formatting(self, formatting: str) -> None: + format_char = FORMAT_CHARS[formatting]
- def clear_tags(self) -> None: _buffer = self.get_buffer() - start, finish = self.get_active_iters() - _buffer.remove_all_tags(start, finish) - - def color_set(self, widget, response): - if response in (-6, -4): - widget.destroy() + start, end = self._get_active_iters() + start_offset = start.get_offset() + end_offset = end.get_offset() + + text = _buffer.get_text(start, end, True) + if text.startswith(format_char) and text.endswith(format_char): + # (Selected) text begins and ends with formatting chars + # -> remove them + _buffer.delete( + start, + _buffer.get_iter_at_offset(start_offset + 1)) + _buffer.delete( + _buffer.get_iter_at_offset(end_offset - 2), + _buffer.get_iter_at_offset(end_offset - 1)) return
- color = widget.get_property('rgba') - widget.destroy() - _buffer = self.get_buffer() - - color_string = convert_rgba_to_hex(color) - tag_name = 'color' + color_string - if not tag_name in self.color_tags: - tag_color = _buffer.create_tag(tag_name) - tag_color.set_property('foreground', color_string) - begin = f'<span style="color: {color_string};">' - self.begin_tags[tag_name] = begin - self.end_tags[tag_name] = '</span>' - self.color_tags.append(tag_name) - - start, finish = self.get_active_iters() - - for tag in self.color_tags: - _buffer.remove_tag_by_name(tag, start, finish) - - _buffer.apply_tag_by_name(tag_name, start, finish) - - def font_set(self, widget, response, start, finish): - if response in (-6, -4): - widget.destroy() + ext_start = _buffer.get_iter_at_offset(start_offset - 1) + ext_end = _buffer.get_iter_at_offset(end_offset + 1) + ext_text = _buffer.get_text(ext_start, ext_end, True) + if ext_text.startswith(format_char) and ext_text.endswith(format_char): + # (Selected) text is surrounded by formatting chars -> remove them + _buffer.delete( + ext_start, + _buffer.get_iter_at_offset(start_offset)) + _buffer.delete( + _buffer.get_iter_at_offset(end_offset - 1), + _buffer.get_iter_at_offset(end_offset)) return
- font = widget.get_font() - font_desc = widget.get_font_desc() - family = font_desc.get_family() - size = font_desc.get_size() - size = size / Pango.SCALE - weight = font_desc.get_weight() - style = font_desc.get_style() - - widget.destroy() - - _buffer = self.get_buffer() - - tag_name = 'font' + font - if not tag_name in self.fonts_tags: - tag_font = _buffer.create_tag(tag_name) - tag_font.set_property('font', f'{family} {size}') - beg = f'<span style="font-family: {family}; font-size: {size}px">' - self.begin_tags[tag_name] = beg - self.end_tags[tag_name] = '</span>' - self.fonts_tags.append(tag_name) - - for tag in self.fonts_tags: - _buffer.remove_tag_by_name(tag, start, finish) - - _buffer.apply_tag_by_name(tag_name, start, finish) - - if weight == Pango.Weight.BOLD: - _buffer.apply_tag_by_name('bold', start, finish) - else: - _buffer.remove_tag_by_name('bold', start, finish) - - if style == Pango.Style.ITALIC: - _buffer.apply_tag_by_name('italic', start, finish) - else: - _buffer.remove_tag_by_name('italic', start, finish) - - def get_xhtml(self) -> Optional[str]: - _buffer = self.get_buffer() - old = _buffer.get_start_iter() - tags = {} - tags['bold'] = False - iter_ = _buffer.get_start_iter() - old = _buffer.get_start_iter() - text = '' - modified = False - - def xhtml_special(text): - text = text.replace('<', '<') - text = text.replace('>', '>') - text = text.replace('&', '&') - text = text.replace('\n', '<br />') - return text - - for tag in iter_.get_toggled_tags(True): - tag_name = tag.get_property('name') - if tag_name not in self.begin_tags: - continue - text += self.begin_tags[tag_name] - modified = True - while (iter_.forward_to_tag_toggle(None) and not iter_.is_end()): - text += xhtml_special(_buffer.get_text(old, iter_, True)) - old.forward_to_tag_toggle(None) - new_tags, old_tags, end_tags = [], [], [] - for tag in iter_.get_toggled_tags(True): - tag_name = tag.get_property('name') - if tag_name not in self.begin_tags: - continue - new_tags.append(tag_name) - modified = True - - for tag in iter_.get_tags(): - tag_name = tag.get_property('name') - if (tag_name not in self.begin_tags or - tag_name not in self.end_tags): - continue - if tag_name not in new_tags: - old_tags.append(tag_name) - - for tag in iter_.get_toggled_tags(False): - tag_name = tag.get_property('name') - if tag_name not in self.end_tags: - continue - end_tags.append(tag_name) - - for tag in old_tags: - text += self.end_tags[tag] - for tag in end_tags: - text += self.end_tags[tag] - for tag in new_tags: - text += self.begin_tags[tag] - for tag in old_tags: - text += self.begin_tags[tag] - - buffer_text = _buffer.get_text(old, _buffer.get_end_iter(), True) - text += xhtml_special(buffer_text) - for tag in iter_.get_toggled_tags(False): - tag_name = tag.get_property('name') - if tag_name not in self.end_tags: - continue - text += self.end_tags[tag_name] - - if modified: - wrapped_text = f'<p>{self.make_clickable_urls(text)}</p>' - return build_xhtml_body(wrapped_text) - return None + # No formatting chars found at start/end or surrounding -> add them + _buffer.insert(start, format_char, -1) + _buffer.insert( + _buffer.get_iter_at_offset(end_offset + 1), + format_char, + -1) + _buffer.select_range( + _buffer.get_iter_at_offset(start_offset), + _buffer.get_iter_at_offset(end_offset + 2))
def replace_emojis(self) -> None: theme = app.settings.get('emoticons_theme') if not theme or theme == 'font': return
- def replace(anchor): + def _replace(anchor): if anchor is None: return image = anchor.get_widgets()[0] if hasattr(image, 'codepoint'): # found emoji - self.replace_char_at_iter(iter_, image.codepoint) + self._replace_char_at_iter(iter_, image.codepoint) image.destroy()
iter_ = self.get_buffer().get_start_iter() - replace(iter_.get_child_anchor()) + _replace(iter_.get_child_anchor())
while iter_.forward_char(): - replace(iter_.get_child_anchor()) + _replace(iter_.get_child_anchor())
- def replace_char_at_iter(self, iter_, new_char): + def _replace_char_at_iter(self, iter_, new_char): buffer_ = self.get_buffer() iter_2 = iter_.copy() iter_2.forward_char() @@ -398,24 +248,18 @@ def insert_emoji(self, codepoint, pixbuf): buffer_.insert_at_cursor(' ')
def clear(self, *args): - """ - Clear text in the textview - """ _buffer = self.get_buffer() start, end = _buffer.get_bounds() _buffer.delete(start, end)
def save_undo(self, text: str) -> None: - self.undo_list.append(text) - if len(self.undo_list) > UNDO_LIMIT: - del self.undo_list[0] + self._undo_list.append(text) + if len(self._undo_list) > UNDO_LIMIT: + del self._undo_list[0] self.undo_pressed = False
def undo(self, *args): - """ - Undo text in the textview - """ _buffer = self.get_buffer() - if self.undo_list: - _buffer.set_text(self.undo_list.pop()) + if self._undo_list: + _buffer.set_text(self._undo_list.pop()) self.undo_pressed = True
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/ee113209fcae27e6eb613bcf96bf05eb...