Daniel Brötzmann pushed to branch master at gajim / gajim
Commits:
-
165dda84
by wurstsalat at 2021-12-13T20:44:58+01:00
-
e9c0416a
by wurstsalat at 2021-12-13T20:53:17+01:00
-
14704ff1
by wurstsalat at 2021-12-13T21:22:38+01:00
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:
| ... | ... | @@ -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>
|
| ... | ... | @@ -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>
|
| ... | ... | @@ -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()
|
| ... | ... | @@ -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)
|
| ... | ... | @@ -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()
|
| ... | ... | @@ -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('<', '<')
|
|
| 296 |
- text = text.replace('>', '>')
|
|
| 297 |
- text = text.replace('&', '&')
|
|
| 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
|