Daniel Brötzmann pushed to branch master at gajim / gajim

Commits:

6 changed files:

Changes:

  • gajim/data/gui/chat_control.ui
    ... ... @@ -51,75 +51,30 @@
    51 51
         <property name="visible">True</property>
    
    52 52
         <property name="can-focus">False</property>
    
    53 53
         <child>
    
    54
    -      <object class="GtkCheckMenuItem" id="bold">
    
    54
    +      <object class="GtkMenuItem" id="bold">
    
    55 55
             <property name="name">bold</property>
    
    56 56
             <property name="visible">True</property>
    
    57 57
             <property name="can-focus">False</property>
    
    58 58
             <property name="label" translatable="yes">Bold</property>
    
    59
    -        <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/>
    
    59
    +        <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/>
    
    60 60
           </object>
    
    61 61
         </child>
    
    62 62
         <child>
    
    63
    -      <object class="GtkCheckMenuItem" id="italic">
    
    63
    +      <object class="GtkMenuItem" id="italic">
    
    64 64
             <property name="name">italic</property>
    
    65 65
             <property name="visible">True</property>
    
    66 66
             <property name="can-focus">False</property>
    
    67 67
             <property name="label" translatable="yes">Italic</property>
    
    68
    -        <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/>
    
    68
    +        <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/>
    
    69 69
           </object>
    
    70 70
         </child>
    
    71 71
         <child>
    
    72
    -      <object class="GtkCheckMenuItem" id="underline">
    
    73
    -        <property name="name">underline</property>
    
    74
    -        <property name="visible">True</property>
    
    75
    -        <property name="can-focus">False</property>
    
    76
    -        <property name="label" translatable="yes">Underline</property>
    
    77
    -        <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/>
    
    78
    -      </object>
    
    79
    -    </child>
    
    80
    -    <child>
    
    81
    -      <object class="GtkCheckMenuItem" id="strike">
    
    72
    +      <object class="GtkMenuItem" id="strike">
    
    82 73
             <property name="name">strike</property>
    
    83 74
             <property name="visible">True</property>
    
    84 75
             <property name="can-focus">False</property>
    
    85 76
             <property name="label" translatable="yes">Strike</property>
    
    86
    -        <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/>
    
    87
    -      </object>
    
    88
    -    </child>
    
    89
    -    <child>
    
    90
    -      <object class="GtkSeparatorMenuItem">
    
    91
    -        <property name="visible">True</property>
    
    92
    -        <property name="can-focus">False</property>
    
    93
    -      </object>
    
    94
    -    </child>
    
    95
    -    <child>
    
    96
    -      <object class="GtkMenuItem" id="color">
    
    97
    -        <property name="visible">True</property>
    
    98
    -        <property name="can-focus">False</property>
    
    99
    -        <property name="label" translatable="yes">Color</property>
    
    100
    -        <signal name="activate" handler="on_color_menuitem_activate" swapped="no"/>
    
    101
    -      </object>
    
    102
    -    </child>
    
    103
    -    <child>
    
    104
    -      <object class="GtkMenuItem" id="font">
    
    105
    -        <property name="visible">True</property>
    
    106
    -        <property name="can-focus">False</property>
    
    107
    -        <property name="label" translatable="yes">Font</property>
    
    108
    -        <signal name="activate" handler="on_font_menuitem_activate" swapped="no"/>
    
    109
    -      </object>
    
    110
    -    </child>
    
    111
    -    <child>
    
    112
    -      <object class="GtkSeparatorMenuItem">
    
    113
    -        <property name="visible">True</property>
    
    114
    -        <property name="can-focus">False</property>
    
    115
    -      </object>
    
    116
    -    </child>
    
    117
    -    <child>
    
    118
    -      <object class="GtkMenuItem" id="clear_formatting">
    
    119
    -        <property name="visible">True</property>
    
    120
    -        <property name="can-focus">False</property>
    
    121
    -        <property name="label" translatable="yes">Clear formatting</property>
    
    122
    -        <signal name="activate" handler="on_clear_formatting_menuitem_activate" swapped="no"/>
    
    77
    +        <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/>
    
    123 78
           </object>
    
    124 79
         </child>
    
    125 80
       </object>
    
    ... ... @@ -400,12 +355,12 @@
    400 355
                             <property name="can-focus">True</property>
    
    401 356
                             <property name="focus-on-click">False</property>
    
    402 357
                             <property name="receives-default">True</property>
    
    403
    -                        <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
    
    404
    -                        <property name="has-tooltip">True</property>
    
    358
    +                        <property name="tooltip-text" translatable="yes">Format your message…</property>
    
    405 359
                             <property name="relief">none</property>
    
    406 360
                             <property name="popup">formattings_menu</property>
    
    361
    +                        <property name="direction">up</property>
    
    407 362
                             <child>
    
    408
    -                          <object class="GtkImage" id="image10">
    
    363
    +                          <object class="GtkImage">
    
    409 364
                                 <property name="visible">True</property>
    
    410 365
                                 <property name="can-focus">False</property>
    
    411 366
                                 <property name="icon-name">format-text-bold-symbolic</property>
    

  • gajim/data/gui/groupchat_control.ui
    ... ... @@ -51,75 +51,30 @@
    51 51
         <property name="visible">True</property>
    
    52 52
         <property name="can-focus">False</property>
    
    53 53
         <child>
    
    54
    -      <object class="GtkCheckMenuItem" id="bold">
    
    54
    +      <object class="GtkMenuItem" id="bold">
    
    55 55
             <property name="name">bold</property>
    
    56 56
             <property name="visible">True</property>
    
    57 57
             <property name="can-focus">False</property>
    
    58 58
             <property name="label" translatable="yes">Bold</property>
    
    59
    -        <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/>
    
    59
    +        <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/>
    
    60 60
           </object>
    
    61 61
         </child>
    
    62 62
         <child>
    
    63
    -      <object class="GtkCheckMenuItem" id="italic">
    
    63
    +      <object class="GtkMenuItem" id="italic">
    
    64 64
             <property name="name">italic</property>
    
    65 65
             <property name="visible">True</property>
    
    66 66
             <property name="can-focus">False</property>
    
    67 67
             <property name="label" translatable="yes">Italic</property>
    
    68
    -        <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/>
    
    68
    +        <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/>
    
    69 69
           </object>
    
    70 70
         </child>
    
    71 71
         <child>
    
    72
    -      <object class="GtkCheckMenuItem" id="underline">
    
    73
    -        <property name="name">underline</property>
    
    74
    -        <property name="visible">True</property>
    
    75
    -        <property name="can-focus">False</property>
    
    76
    -        <property name="label" translatable="yes">Underline</property>
    
    77
    -        <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/>
    
    78
    -      </object>
    
    79
    -    </child>
    
    80
    -    <child>
    
    81
    -      <object class="GtkCheckMenuItem" id="strike">
    
    72
    +      <object class="GtkMenuItem" id="strike">
    
    82 73
             <property name="name">strike</property>
    
    83 74
             <property name="visible">True</property>
    
    84 75
             <property name="can-focus">False</property>
    
    85 76
             <property name="label" translatable="yes">Strike</property>
    
    86
    -        <signal name="activate" handler="on_formatting_menuitem_activate" swapped="no"/>
    
    87
    -      </object>
    
    88
    -    </child>
    
    89
    -    <child>
    
    90
    -      <object class="GtkSeparatorMenuItem">
    
    91
    -        <property name="visible">True</property>
    
    92
    -        <property name="can-focus">False</property>
    
    93
    -      </object>
    
    94
    -    </child>
    
    95
    -    <child>
    
    96
    -      <object class="GtkMenuItem" id="color">
    
    97
    -        <property name="visible">True</property>
    
    98
    -        <property name="can-focus">False</property>
    
    99
    -        <property name="label" translatable="yes">Color</property>
    
    100
    -        <signal name="activate" handler="on_color_menuitem_activate" swapped="no"/>
    
    101
    -      </object>
    
    102
    -    </child>
    
    103
    -    <child>
    
    104
    -      <object class="GtkMenuItem" id="font">
    
    105
    -        <property name="visible">True</property>
    
    106
    -        <property name="can-focus">False</property>
    
    107
    -        <property name="label" translatable="yes">Font</property>
    
    108
    -        <signal name="activate" handler="on_font_menuitem_activate" swapped="no"/>
    
    109
    -      </object>
    
    110
    -    </child>
    
    111
    -    <child>
    
    112
    -      <object class="GtkSeparatorMenuItem">
    
    113
    -        <property name="visible">True</property>
    
    114
    -        <property name="can-focus">False</property>
    
    115
    -      </object>
    
    116
    -    </child>
    
    117
    -    <child>
    
    118
    -      <object class="GtkMenuItem" id="clear_formatting">
    
    119
    -        <property name="visible">True</property>
    
    120
    -        <property name="can-focus">False</property>
    
    121
    -        <property name="label" translatable="yes">Clear formatting</property>
    
    122
    -        <signal name="activate" handler="on_clear_formatting_menuitem_activate" swapped="no"/>
    
    77
    +        <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/>
    
    123 78
           </object>
    
    124 79
         </child>
    
    125 80
       </object>
    
    ... ... @@ -584,14 +539,13 @@
    584 539
                             <child>
    
    585 540
                               <object class="GtkMenuButton" id="formattings_button">
    
    586 541
                                 <property name="visible">True</property>
    
    587
    -                            <property name="sensitive">False</property>
    
    588 542
                                 <property name="can-focus">True</property>
    
    589 543
                                 <property name="focus-on-click">False</property>
    
    590 544
                                 <property name="receives-default">True</property>
    
    591
    -                            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
    
    592
    -                            <property name="has-tooltip">True</property>
    
    545
    +                            <property name="tooltip-text" translatable="yes">Format your message</property>
    
    593 546
                                 <property name="relief">none</property>
    
    594 547
                                 <property name="popup">formattings_menu</property>
    
    548
    +                            <property name="direction">up</property>
    
    595 549
                                 <child>
    
    596 550
                                   <object class="GtkImage">
    
    597 551
                                     <property name="visible">True</property>
    

  • gajim/gtk/controls/base.py
    ... ... @@ -1024,14 +1024,12 @@ def get_our_nick(self) -> str:
    1024 1024
         def _on_send_message(self, *args):
    
    1025 1025
             self.msg_textview.replace_emojis()
    
    1026 1026
             message = self.msg_textview.get_text()
    
    1027
    -        xhtml = self.msg_textview.get_xhtml()
    
    1028
    -        self.send_message(message, xhtml=xhtml)
    
    1027
    +        self.send_message(message)
    
    1029 1028
     
    
    1030 1029
         def send_message(self,
    
    1031 1030
                          message: str,
    
    1032 1031
                          type_: str = 'chat',
    
    1033 1032
                          resource: Optional[str] = None,
    
    1034
    -                     xhtml: Optional[str] = None,
    
    1035 1033
                          process_commands: bool = True,
    
    1036 1034
                          attention: bool = False
    
    1037 1035
                          ) -> None:
    
    ... ... @@ -1065,8 +1063,7 @@ def send_message(self,
    1065 1063
                                        label=label,
    
    1066 1064
                                        control=self,
    
    1067 1065
                                        attention=attention,
    
    1068
    -                                   correct_id=correct_id,
    
    1069
    -                                   xhtml=xhtml)
    
    1066
    +                                   correct_id=correct_id)
    
    1070 1067
     
    
    1071 1068
             self._client.send_message(message_)
    
    1072 1069
     
    
    ... ... @@ -1076,9 +1073,7 @@ def send_message(self,
    1076 1073
             # Be sure to send user nickname only once according to JEP-0172
    
    1077 1074
             self.user_nick = None
    
    1078 1075
     
    
    1079
    -        # Clear msg input
    
    1080
    -        message_buffer = self.msg_textview.get_buffer()
    
    1081
    -        message_buffer.set_text('')  # clear message buffer (and tv of course)
    
    1076
    +        self.msg_textview.clear()
    
    1082 1077
     
    
    1083 1078
         def _on_message_tv_buffer_changed(self, textbuffer):
    
    1084 1079
             has_text = self.msg_textview.has_text()
    
    ... ... @@ -1303,25 +1298,11 @@ def _on_emoticon_button_clicked(self, _widget):
    1303 1298
             self.msg_textview.emit('insert-emoji')
    
    1304 1299
             self.xml.emoticons_button.set_property('active', False)
    
    1305 1300
     
    
    1306
    -    def on_color_menuitem_activate(self, _widget):
    
    1307
    -        color_dialog = Gtk.ColorChooserDialog(None, app.window)
    
    1308
    -        color_dialog.set_use_alpha(False)
    
    1309
    -        color_dialog.connect('response', self.msg_textview.color_set)
    
    1310
    -        color_dialog.show_all()
    
    1311
    -
    
    1312
    -    def on_font_menuitem_activate(self, _widget):
    
    1313
    -        font_dialog = Gtk.FontChooserDialog(None, app.window)
    
    1314
    -        start, finish = self.msg_textview.get_active_iters()
    
    1315
    -        font_dialog.connect(
    
    1316
    -            'response', self.msg_textview.font_set, start, finish)
    
    1317
    -        font_dialog.show_all()
    
    1318
    -
    
    1319
    -    def on_formatting_menuitem_activate(self, widget):
    
    1320
    -        tag = widget.get_name()
    
    1321
    -        self.msg_textview.set_tag(tag)
    
    1322
    -
    
    1323
    -    def on_clear_formatting_menuitem_activate(self, _widget):
    
    1324
    -        self.msg_textview.clear_tags()
    
    1301
    +    def _on_formatting_menuitem_activate(self,
    
    1302
    +                                         menu_item: Gtk.CheckMenuItem
    
    1303
    +                                         ) -> None:
    
    1304
    +        formatting = menu_item.get_name()
    
    1305
    +        self.msg_textview.apply_formatting(formatting)
    
    1325 1306
     
    
    1326 1307
         def _style_changed(self, *args):
    
    1327 1308
             self.update_text_tags()
    

  • gajim/gtk/controls/chat.py
    ... ... @@ -307,18 +307,6 @@ def _on_send_marker(self, action, param):
    307 307
             action.set_state(param)
    
    308 308
             self.contact.settings.set('send_marker', param.get_boolean())
    
    309 309
     
    
    310
    -    def _update_toolbar(self) -> None:
    
    311
    -        # Formatting
    
    312
    -        # TODO: find out what encryption allows for xhtml and which not
    
    313
    -        if self.contact.supports(Namespace.XHTML_IM):
    
    314
    -            self.xml.formattings_button.set_sensitive(True)
    
    315
    -            self.xml.formattings_button.set_tooltip_text(_(
    
    316
    -                'Show a list of formattings'))
    
    317
    -        else:
    
    318
    -            self.xml.formattings_button.set_sensitive(False)
    
    319
    -            self.xml.formattings_button.set_tooltip_text(
    
    320
    -                _('This contact does not support HTML'))
    
    321
    -
    
    322 310
         def update_all_pep_types(self) -> None:
    
    323 311
             self._update_pep(PEPEventType.LOCATION)
    
    324 312
             self._update_pep(PEPEventType.TUNE)
    

  • gajim/gtk/controls/groupchat.py
    ... ... @@ -883,9 +883,6 @@ def _set_control_active(self) -> None:
    883 883
             self.msg_textview.set_editable(True)
    
    884 884
     
    
    885 885
             self.roster.initial_draw()
    
    886
    -
    
    887
    -        self.xml.formattings_button.set_sensitive(True)
    
    888
    -
    
    889 886
             self.conversation_view.update_avatars()
    
    890 887
     
    
    891 888
             self.update_actions()
    

  • gajim/gtk/message_input.py
    ... ... @@ -17,7 +17,6 @@
    17 17
     # You should have received a copy of the GNU General Public License
    
    18 18
     # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
    
    19 19
     
    
    20
    -from typing import Optional
    
    21 20
     from typing import Dict
    
    22 21
     from typing import List
    
    23 22
     from typing import Tuple
    
    ... ... @@ -27,12 +26,9 @@
    27 26
     from gi.repository import GLib
    
    28 27
     from gi.repository import Pango
    
    29 28
     
    
    30
    -from nbxmpp.modules.misc import build_xhtml_body
    
    31
    -
    
    32 29
     from gajim.common import app
    
    33
    -from gajim.common.regex import LINK_REGEX
    
    30
    +from gajim.common.styling import process
    
    34 31
     
    
    35
    -from .util import convert_rgba_to_hex
    
    36 32
     from .util import scroll_to_end
    
    37 33
     
    
    38 34
     if app.is_installed('GSPELL'):
    
    ... ... @@ -40,14 +36,20 @@
    40 36
     
    
    41 37
     UNDO_LIMIT: int = 20
    
    42 38
     
    
    39
    +FORMAT_CHARS: Dict[str, str] = {
    
    40
    +    'bold': '*',
    
    41
    +    'italic': '_',
    
    42
    +    'strike': '~',
    
    43
    +    'pre': '`',
    
    44
    +}
    
    45
    +
    
    43 46
     
    
    44 47
     class MessageInputTextView(Gtk.TextView):
    
    45 48
         """
    
    46 49
         A Gtk.Textview for chat message input
    
    47 50
         """
    
    48
    -    def __init__(self):
    
    51
    +    def __init__(self) -> None:
    
    49 52
             Gtk.TextView.__init__(self)
    
    50
    -
    
    51 53
             self.set_border_width(3)
    
    52 54
             self.set_accepts_tab(True)
    
    53 55
             self.set_editable(True)
    
    ... ... @@ -61,36 +63,15 @@ def __init__(self):
    61 63
     
    
    62 64
             self.drag_dest_unset()
    
    63 65
     
    
    64
    -        self.undo_list: List[str] = []
    
    66
    +        self._undo_list: List[str] = []
    
    65 67
             self.undo_pressed: bool = False
    
    66 68
     
    
    67
    -        self.begin_tags: Dict[str, str] = {}
    
    68
    -        self.end_tags: Dict[str, str] = {}
    
    69
    -        self.color_tags: List[str] = []
    
    70
    -        self.fonts_tags: List[str] = []
    
    71
    -        self.other_tags: Dict[str, Gtk.TextTag] = {}
    
    72
    -
    
    73
    -        buffer_ = self.get_buffer()
    
    74
    -        self.other_tags['bold'] = buffer_.create_tag('bold')
    
    75
    -        self.other_tags['bold'].set_property('weight', Pango.Weight.BOLD)
    
    76
    -        self.begin_tags['bold'] = '<strong>'
    
    77
    -        self.end_tags['bold'] = '</strong>'
    
    78
    -        self.other_tags['italic'] = buffer_.create_tag('italic')
    
    79
    -        self.other_tags['italic'].set_property('style', Pango.Style.ITALIC)
    
    80
    -        self.begin_tags['italic'] = '<em>'
    
    81
    -        self.end_tags['italic'] = '</em>'
    
    82
    -        self.other_tags['underline'] = buffer_.create_tag('underline')
    
    83
    -        self.other_tags['underline'].set_property(
    
    84
    -            'underline', Pango.Underline.SINGLE)
    
    85
    -        underline = '<span style="text-decoration: underline;">'
    
    86
    -        self.begin_tags['underline'] = underline
    
    87
    -        self.end_tags['underline'] = '</span>'
    
    88
    -        self.other_tags['strike'] = buffer_.create_tag('strike')
    
    89
    -        self.other_tags['strike'].set_property('strikethrough', True)
    
    90
    -        strike = '<span style="text-decoration: line-through;">'
    
    91
    -        self.begin_tags['strike'] = strike
    
    92
    -        self.end_tags['strike'] = '</span>'
    
    69
    +        self.get_buffer().create_tag('strong', weight=Pango.Weight.BOLD)
    
    70
    +        self.get_buffer().create_tag('emphasis', style=Pango.Style.ITALIC)
    
    71
    +        self.get_buffer().create_tag('strike', strikethrough=True)
    
    72
    +        self.get_buffer().create_tag('pre', family='monospace')
    
    93 73
     
    
    74
    +        self.get_buffer().connect('changed', self._on_text_changed)
    
    94 75
             self.connect_after('paste-clipboard', self._after_paste_clipboard)
    
    95 76
             self.connect('focus-in-event', self._on_focus_in)
    
    96 77
             self.connect('focus-out-event', self._on_focus_out)
    
    ... ... @@ -120,6 +101,18 @@ def _on_focus_out(self, _widget, _event):
    120 101
                 self.toggle_speller(False)
    
    121 102
             return False
    
    122 103
     
    
    104
    +    def _on_text_changed(self, buffer_):
    
    105
    +        text = self.get_text()
    
    106
    +        if not text:
    
    107
    +            return
    
    108
    +
    
    109
    +        result = process(text)
    
    110
    +        for block in result.blocks:
    
    111
    +            for span in block.spans:
    
    112
    +                start_iter = buffer_.get_iter_at_offset(span.start)
    
    113
    +                end_iter = buffer_.get_iter_at_offset(span.end)
    
    114
    +                buffer_.apply_tag_by_name(span.name, start_iter, end_iter)
    
    115
    +
    
    123 116
         def insert_text(self, text: str) -> None:
    
    124 117
             self.get_buffer().insert_at_cursor(text)
    
    125 118
     
    
    ... ... @@ -149,229 +142,86 @@ def toggle_speller(self, activate: bool) -> None:
    149 142
                 spell_view.set_inline_spell_checking(activate)
    
    150 143
     
    
    151 144
         @staticmethod
    
    152
    -    def _after_paste_clipboard(textview):
    
    145
    +    def _after_paste_clipboard(textview: Gtk.TextView) -> None:
    
    153 146
             buffer_ = textview.get_buffer()
    
    154 147
             mark = buffer_.get_insert()
    
    155 148
             iter_ = buffer_.get_iter_at_mark(mark)
    
    156 149
             if iter_.get_offset() == buffer_.get_end_iter().get_offset():
    
    157 150
                 GLib.idle_add(scroll_to_end, textview.get_parent())
    
    158 151
     
    
    159
    -    def make_clickable_urls(self, text: str) -> str:
    
    160
    -        _buffer = self.get_buffer()
    
    161
    -
    
    162
    -        start = 0
    
    163
    -        end = 0
    
    164
    -        index = 0
    
    165
    -
    
    166
    -        new_text = ''
    
    167
    -        iterator = LINK_REGEX.finditer(text)
    
    168
    -        for match in iterator:
    
    169
    -            start, end = match.span()
    
    170
    -            url = text[start:end]
    
    171
    -            if start != 0:
    
    172
    -                text_before_special_text = text[index:start]
    
    173
    -            else:
    
    174
    -                text_before_special_text = ''
    
    175
    -            # we insert normal text
    
    176
    -            new_text += f'{text_before_special_text}<a href="{url}">{url}</a>'
    
    177
    -            index = end  # update index
    
    178
    -
    
    179
    -        if end < len(text):
    
    180
    -            new_text += text[end:]
    
    181
    -
    
    182
    -        return new_text  # the position after *last* special text
    
    183
    -
    
    184
    -    def get_active_tags(self) -> List[str]:
    
    185
    -        start = self.get_active_iters()[0]
    
    186
    -        active_tags = []
    
    187
    -        for tag in start.get_tags():
    
    188
    -            active_tags.append(tag.get_property('name'))
    
    189
    -        return active_tags
    
    190
    -
    
    191
    -    def get_active_iters(self) -> Tuple[Gtk.TextIter, Gtk.TextIter]:
    
    152
    +    def _get_active_iters(self) -> Tuple[Gtk.TextIter, Gtk.TextIter]:
    
    192 153
             _buffer = self.get_buffer()
    
    193 154
             return_val = _buffer.get_selection_bounds()
    
    194
    -        if return_val: # if sth was selected
    
    195
    -            start, finish = return_val[0], return_val[1]
    
    155
    +        if return_val:  # if something is selected
    
    156
    +            start, end = return_val[0], return_val[1]
    
    196 157
             else:
    
    197
    -            start, finish = _buffer.get_bounds()
    
    198
    -        return (start, finish)
    
    158
    +            start, end = _buffer.get_bounds()
    
    159
    +        return (start, end)
    
    199 160
     
    
    200
    -    def set_tag(self, tag: str) -> None:
    
    201
    -        _buffer = self.get_buffer()
    
    202
    -        start, finish = self.get_active_iters()
    
    203
    -        if start.has_tag(self.other_tags[tag]):
    
    204
    -            _buffer.remove_tag_by_name(tag, start, finish)
    
    205
    -        else:
    
    206
    -            if tag == 'underline':
    
    207
    -                _buffer.remove_tag_by_name('strike', start, finish)
    
    208
    -            elif tag == 'strike':
    
    209
    -                _buffer.remove_tag_by_name('underline', start, finish)
    
    210
    -            _buffer.apply_tag_by_name(tag, start, finish)
    
    161
    +    def apply_formatting(self, formatting: str) -> None:
    
    162
    +        format_char = FORMAT_CHARS[formatting]
    
    211 163
     
    
    212
    -    def clear_tags(self) -> None:
    
    213 164
             _buffer = self.get_buffer()
    
    214
    -        start, finish = self.get_active_iters()
    
    215
    -        _buffer.remove_all_tags(start, finish)
    
    216
    -
    
    217
    -    def color_set(self, widget, response):
    
    218
    -        if response in (-6, -4):
    
    219
    -            widget.destroy()
    
    165
    +        start, end = self._get_active_iters()
    
    166
    +        start_offset = start.get_offset()
    
    167
    +        end_offset = end.get_offset()
    
    168
    +
    
    169
    +        text = _buffer.get_text(start, end, True)
    
    170
    +        if text.startswith(format_char) and text.endswith(format_char):
    
    171
    +            # (Selected) text begins and ends with formatting chars
    
    172
    +            # -> remove them
    
    173
    +            _buffer.delete(
    
    174
    +                start,
    
    175
    +                _buffer.get_iter_at_offset(start_offset + 1))
    
    176
    +            _buffer.delete(
    
    177
    +                _buffer.get_iter_at_offset(end_offset - 2),
    
    178
    +                _buffer.get_iter_at_offset(end_offset - 1))
    
    220 179
                 return
    
    221 180
     
    
    222
    -        color = widget.get_property('rgba')
    
    223
    -        widget.destroy()
    
    224
    -        _buffer = self.get_buffer()
    
    225
    -
    
    226
    -        color_string = convert_rgba_to_hex(color)
    
    227
    -        tag_name = 'color' + color_string
    
    228
    -        if not tag_name in self.color_tags:
    
    229
    -            tag_color = _buffer.create_tag(tag_name)
    
    230
    -            tag_color.set_property('foreground', color_string)
    
    231
    -            begin = f'<span style="color: {color_string};">'
    
    232
    -            self.begin_tags[tag_name] = begin
    
    233
    -            self.end_tags[tag_name] = '</span>'
    
    234
    -            self.color_tags.append(tag_name)
    
    235
    -
    
    236
    -        start, finish = self.get_active_iters()
    
    237
    -
    
    238
    -        for tag in self.color_tags:
    
    239
    -            _buffer.remove_tag_by_name(tag, start, finish)
    
    240
    -
    
    241
    -        _buffer.apply_tag_by_name(tag_name, start, finish)
    
    242
    -
    
    243
    -    def font_set(self, widget, response, start, finish):
    
    244
    -        if response in (-6, -4):
    
    245
    -            widget.destroy()
    
    181
    +        ext_start = _buffer.get_iter_at_offset(start_offset - 1)
    
    182
    +        ext_end = _buffer.get_iter_at_offset(end_offset + 1)
    
    183
    +        ext_text = _buffer.get_text(ext_start, ext_end, True)
    
    184
    +        if ext_text.startswith(format_char) and ext_text.endswith(format_char):
    
    185
    +            # (Selected) text is surrounded by formatting chars -> remove them
    
    186
    +            _buffer.delete(
    
    187
    +                ext_start,
    
    188
    +                _buffer.get_iter_at_offset(start_offset))
    
    189
    +            _buffer.delete(
    
    190
    +                _buffer.get_iter_at_offset(end_offset - 1),
    
    191
    +                _buffer.get_iter_at_offset(end_offset))
    
    246 192
                 return
    
    247 193
     
    
    248
    -        font = widget.get_font()
    
    249
    -        font_desc = widget.get_font_desc()
    
    250
    -        family = font_desc.get_family()
    
    251
    -        size = font_desc.get_size()
    
    252
    -        size = size / Pango.SCALE
    
    253
    -        weight = font_desc.get_weight()
    
    254
    -        style = font_desc.get_style()
    
    255
    -
    
    256
    -        widget.destroy()
    
    257
    -
    
    258
    -        _buffer = self.get_buffer()
    
    259
    -
    
    260
    -        tag_name = 'font' + font
    
    261
    -        if not tag_name in self.fonts_tags:
    
    262
    -            tag_font = _buffer.create_tag(tag_name)
    
    263
    -            tag_font.set_property('font', f'{family} {size}')
    
    264
    -            beg = f'<span style="font-family: {family}; font-size: {size}px">'
    
    265
    -            self.begin_tags[tag_name] = beg
    
    266
    -            self.end_tags[tag_name] = '</span>'
    
    267
    -            self.fonts_tags.append(tag_name)
    
    268
    -
    
    269
    -        for tag in self.fonts_tags:
    
    270
    -            _buffer.remove_tag_by_name(tag, start, finish)
    
    271
    -
    
    272
    -        _buffer.apply_tag_by_name(tag_name, start, finish)
    
    273
    -
    
    274
    -        if weight == Pango.Weight.BOLD:
    
    275
    -            _buffer.apply_tag_by_name('bold', start, finish)
    
    276
    -        else:
    
    277
    -            _buffer.remove_tag_by_name('bold', start, finish)
    
    278
    -
    
    279
    -        if style == Pango.Style.ITALIC:
    
    280
    -            _buffer.apply_tag_by_name('italic', start, finish)
    
    281
    -        else:
    
    282
    -            _buffer.remove_tag_by_name('italic', start, finish)
    
    283
    -
    
    284
    -    def get_xhtml(self) -> Optional[str]:
    
    285
    -        _buffer = self.get_buffer()
    
    286
    -        old = _buffer.get_start_iter()
    
    287
    -        tags = {}
    
    288
    -        tags['bold'] = False
    
    289
    -        iter_ = _buffer.get_start_iter()
    
    290
    -        old = _buffer.get_start_iter()
    
    291
    -        text = ''
    
    292
    -        modified = False
    
    293
    -
    
    294
    -        def xhtml_special(text):
    
    295
    -            text = text.replace('<', '&lt;')
    
    296
    -            text = text.replace('>', '&gt;')
    
    297
    -            text = text.replace('&', '&amp;')
    
    298
    -            text = text.replace('\n', '<br />')
    
    299
    -            return text
    
    300
    -
    
    301
    -        for tag in iter_.get_toggled_tags(True):
    
    302
    -            tag_name = tag.get_property('name')
    
    303
    -            if tag_name not in self.begin_tags:
    
    304
    -                continue
    
    305
    -            text += self.begin_tags[tag_name]
    
    306
    -            modified = True
    
    307
    -        while (iter_.forward_to_tag_toggle(None) and not iter_.is_end()):
    
    308
    -            text += xhtml_special(_buffer.get_text(old, iter_, True))
    
    309
    -            old.forward_to_tag_toggle(None)
    
    310
    -            new_tags, old_tags, end_tags = [], [], []
    
    311
    -            for tag in iter_.get_toggled_tags(True):
    
    312
    -                tag_name = tag.get_property('name')
    
    313
    -                if tag_name not in self.begin_tags:
    
    314
    -                    continue
    
    315
    -                new_tags.append(tag_name)
    
    316
    -                modified = True
    
    317
    -
    
    318
    -            for tag in iter_.get_tags():
    
    319
    -                tag_name = tag.get_property('name')
    
    320
    -                if (tag_name not in self.begin_tags or
    
    321
    -                        tag_name not in self.end_tags):
    
    322
    -                    continue
    
    323
    -                if tag_name not in new_tags:
    
    324
    -                    old_tags.append(tag_name)
    
    325
    -
    
    326
    -            for tag in iter_.get_toggled_tags(False):
    
    327
    -                tag_name = tag.get_property('name')
    
    328
    -                if tag_name not in self.end_tags:
    
    329
    -                    continue
    
    330
    -                end_tags.append(tag_name)
    
    331
    -
    
    332
    -            for tag in old_tags:
    
    333
    -                text += self.end_tags[tag]
    
    334
    -            for tag in end_tags:
    
    335
    -                text += self.end_tags[tag]
    
    336
    -            for tag in new_tags:
    
    337
    -                text += self.begin_tags[tag]
    
    338
    -            for tag in old_tags:
    
    339
    -                text += self.begin_tags[tag]
    
    340
    -
    
    341
    -        buffer_text = _buffer.get_text(old, _buffer.get_end_iter(), True)
    
    342
    -        text += xhtml_special(buffer_text)
    
    343
    -        for tag in iter_.get_toggled_tags(False):
    
    344
    -            tag_name = tag.get_property('name')
    
    345
    -            if tag_name not in self.end_tags:
    
    346
    -                continue
    
    347
    -            text += self.end_tags[tag_name]
    
    348
    -
    
    349
    -        if modified:
    
    350
    -            wrapped_text = f'<p>{self.make_clickable_urls(text)}</p>'
    
    351
    -            return build_xhtml_body(wrapped_text)
    
    352
    -        return None
    
    194
    +        # No formatting chars found at start/end or surrounding -> add them
    
    195
    +        _buffer.insert(start, format_char, -1)
    
    196
    +        _buffer.insert(
    
    197
    +            _buffer.get_iter_at_offset(end_offset + 1),
    
    198
    +            format_char,
    
    199
    +            -1)
    
    200
    +        _buffer.select_range(
    
    201
    +            _buffer.get_iter_at_offset(start_offset),
    
    202
    +            _buffer.get_iter_at_offset(end_offset + 2))
    
    353 203
     
    
    354 204
         def replace_emojis(self) -> None:
    
    355 205
             theme = app.settings.get('emoticons_theme')
    
    356 206
             if not theme or theme == 'font':
    
    357 207
                 return
    
    358 208
     
    
    359
    -        def replace(anchor):
    
    209
    +        def _replace(anchor):
    
    360 210
                 if anchor is None:
    
    361 211
                     return
    
    362 212
                 image = anchor.get_widgets()[0]
    
    363 213
                 if hasattr(image, 'codepoint'):
    
    364 214
                     # found emoji
    
    365
    -                self.replace_char_at_iter(iter_, image.codepoint)
    
    215
    +                self._replace_char_at_iter(iter_, image.codepoint)
    
    366 216
                     image.destroy()
    
    367 217
     
    
    368 218
             iter_ = self.get_buffer().get_start_iter()
    
    369
    -        replace(iter_.get_child_anchor())
    
    219
    +        _replace(iter_.get_child_anchor())
    
    370 220
     
    
    371 221
             while iter_.forward_char():
    
    372
    -            replace(iter_.get_child_anchor())
    
    222
    +            _replace(iter_.get_child_anchor())
    
    373 223
     
    
    374
    -    def replace_char_at_iter(self, iter_, new_char):
    
    224
    +    def _replace_char_at_iter(self, iter_, new_char):
    
    375 225
             buffer_ = self.get_buffer()
    
    376 226
             iter_2 = iter_.copy()
    
    377 227
             iter_2.forward_char()
    
    ... ... @@ -398,24 +248,18 @@ def insert_emoji(self, codepoint, pixbuf):
    398 248
             buffer_.insert_at_cursor(' ')
    
    399 249
     
    
    400 250
         def clear(self, *args):
    
    401
    -        """
    
    402
    -        Clear text in the textview
    
    403
    -        """
    
    404 251
             _buffer = self.get_buffer()
    
    405 252
             start, end = _buffer.get_bounds()
    
    406 253
             _buffer.delete(start, end)
    
    407 254
     
    
    408 255
         def save_undo(self, text: str) -> None:
    
    409
    -        self.undo_list.append(text)
    
    410
    -        if len(self.undo_list) > UNDO_LIMIT:
    
    411
    -            del self.undo_list[0]
    
    256
    +        self._undo_list.append(text)
    
    257
    +        if len(self._undo_list) > UNDO_LIMIT:
    
    258
    +            del self._undo_list[0]
    
    412 259
             self.undo_pressed = False
    
    413 260
     
    
    414 261
         def undo(self, *args):
    
    415
    -        """
    
    416
    -        Undo text in the textview
    
    417
    -        """
    
    418 262
             _buffer = self.get_buffer()
    
    419
    -        if self.undo_list:
    
    420
    -            _buffer.set_text(self.undo_list.pop())
    
    263
    +        if self._undo_list:
    
    264
    +            _buffer.set_text(self._undo_list.pop())
    
    421 265
             self.undo_pressed = True