1# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org> 2# Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com> 3# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com> 4# Copyright (C) 2008-2009 Julien Pivotto <roidelapluie AT gmail.com> 5# 6# This file is part of Gajim. 7# 8# Gajim is free software; you can redistribute it and/or modify 9# it under the terms of the GNU General Public License as published 10# by the Free Software Foundation; version 3 only. 11# 12# Gajim is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with Gajim. If not, see <http://www.gnu.org/licenses/>. 19 20from gi.repository import Gtk 21from gi.repository import Gdk 22from gi.repository import GLib 23from gi.repository import Pango 24 25from nbxmpp.modules.misc import build_xhtml_body 26 27from gajim.common import app 28from gajim.common.regex import LINK_REGEX 29 30from .util import scroll_to_end 31 32if app.is_installed('GSPELL'): 33 from gi.repository import Gspell # pylint: disable=ungrouped-imports 34 35 36class MessageInputTextView(Gtk.TextView): 37 """ 38 Class for the message textview (where user writes new messages) for 39 chat/groupchat windows 40 """ 41 UNDO_LIMIT = 20 42 43 def __init__(self): 44 Gtk.TextView.__init__(self) 45 46 # set properties 47 self.set_border_width(3) 48 self.set_accepts_tab(True) 49 self.set_editable(True) 50 self.set_cursor_visible(True) 51 self.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) 52 self.set_left_margin(2) 53 self.set_right_margin(2) 54 self.set_pixels_above_lines(2) 55 self.set_pixels_below_lines(2) 56 self.get_style_context().add_class('gajim-conversation-font') 57 58 self.drag_dest_unset() 59 60 # set undo list 61 self.undo_list = [] 62 # needed to know if we undid something 63 self.undo_pressed = False 64 65 self._last_text = '' 66 67 self.begin_tags = {} 68 self.end_tags = {} 69 self.color_tags = [] 70 self.fonts_tags = [] 71 self.other_tags = {} 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('underline', 84 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>' 93 94 self.connect_after('paste-clipboard', self._after_paste_clipboard) 95 self.connect('focus-in-event', self._on_focus_in) 96 self.connect('focus-out-event', self._on_focus_out) 97 self.connect('destroy', self._on_destroy) 98 99 def _on_destroy(self, *args): 100 # We restore the TextView’s drag destination to avoid a GTK warning 101 # when closing the control. ChatControlBase.shutdown() calls destroy() 102 # on the control’s main box, causing GTK to recursively destroy the 103 # child widgets. GTK then tries to set a target list on the TextView, 104 # resulting in a warning because the Widget has no drag destination. 105 self.drag_dest_set( 106 Gtk.DestDefaults.ALL, 107 None, 108 Gdk.DragAction.DEFAULT) 109 110 def _on_focus_in(self, _widget, _event): 111 self.toggle_speller(True) 112 scrolled = self.get_parent() 113 scrolled.get_style_context().add_class('message-input-focus') 114 return False 115 116 def _on_focus_out(self, _widget, _event): 117 scrolled = self.get_parent() 118 scrolled.get_style_context().remove_class('message-input-focus') 119 if not self.has_text(): 120 self.toggle_speller(False) 121 return False 122 123 def insert_text(self, text): 124 self.get_buffer().insert_at_cursor(text) 125 126 def insert_newline(self): 127 buffer_ = self.get_buffer() 128 buffer_.insert_at_cursor('\n') 129 mark = buffer_.get_insert() 130 iter_ = buffer_.get_iter_at_mark(mark) 131 if buffer_.get_end_iter().equal(iter_): 132 GLib.idle_add(scroll_to_end, self.get_parent()) 133 134 def has_text(self): 135 buf = self.get_buffer() 136 start, end = buf.get_bounds() 137 text = buf.get_text(start, end, True) 138 return text != '' 139 140 def get_text(self): 141 buf = self.get_buffer() 142 start, end = buf.get_bounds() 143 text = self.get_buffer().get_text(start, end, True) 144 return text 145 146 def toggle_speller(self, activate): 147 if app.is_installed('GSPELL') and app.settings.get('use_speller'): 148 spell_view = Gspell.TextView.get_from_gtk_text_view(self) 149 spell_view.set_inline_spell_checking(activate) 150 151 @staticmethod 152 def _after_paste_clipboard(textview): 153 buffer_ = textview.get_buffer() 154 mark = buffer_.get_insert() 155 iter_ = buffer_.get_iter_at_mark(mark) 156 if iter_.get_offset() == buffer_.get_end_iter().get_offset(): 157 GLib.idle_add(scroll_to_end, textview.get_parent()) 158 159 def make_clickable_urls(self, text): 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 += text_before_special_text + \ 177 '<a href="'+ url +'">' + url + '</a>' 178 179 index = end # update index 180 181 if end < len(text): 182 new_text += text[end:] 183 184 return new_text # the position after *last* special text 185 186 def get_active_tags(self): 187 start = self.get_active_iters()[0] 188 active_tags = [] 189 for tag in start.get_tags(): 190 active_tags.append(tag.get_property('name')) 191 return active_tags 192 193 def get_active_iters(self): 194 _buffer = self.get_buffer() 195 return_val = _buffer.get_selection_bounds() 196 if return_val: # if sth was selected 197 start, finish = return_val[0], return_val[1] 198 else: 199 start, finish = _buffer.get_bounds() 200 return (start, finish) 201 202 def set_tag(self, tag): 203 _buffer = self.get_buffer() 204 start, finish = self.get_active_iters() 205 if start.has_tag(self.other_tags[tag]): 206 _buffer.remove_tag_by_name(tag, start, finish) 207 else: 208 if tag == 'underline': 209 _buffer.remove_tag_by_name('strike', start, finish) 210 elif tag == 'strike': 211 _buffer.remove_tag_by_name('underline', start, finish) 212 _buffer.apply_tag_by_name(tag, start, finish) 213 214 def clear_tags(self): 215 _buffer = self.get_buffer() 216 start, finish = self.get_active_iters() 217 _buffer.remove_all_tags(start, finish) 218 219 def color_set(self, widget, response): 220 if response in (-6, -4): 221 widget.destroy() 222 return 223 224 color = widget.get_property('rgba') 225 widget.destroy() 226 _buffer = self.get_buffer() 227 # Create #aabbcc color string from rgba color 228 color_string = '#%02X%02X%02X' % (round(color.red*255), 229 round(color.green*255), 230 round(color.blue*255)) 231 232 tag_name = 'color' + color_string 233 if not tag_name in self.color_tags: 234 tag_color = _buffer.create_tag(tag_name) 235 tag_color.set_property('foreground', color_string) 236 begin = '<span style="color: %s;">' % color_string 237 self.begin_tags[tag_name] = begin 238 self.end_tags[tag_name] = '</span>' 239 self.color_tags.append(tag_name) 240 241 start, finish = self.get_active_iters() 242 243 for tag in self.color_tags: 244 _buffer.remove_tag_by_name(tag, start, finish) 245 246 _buffer.apply_tag_by_name(tag_name, start, finish) 247 248 def font_set(self, widget, response, start, finish): 249 if response in (-6, -4): 250 widget.destroy() 251 return 252 253 font = widget.get_font() 254 font_desc = widget.get_font_desc() 255 family = font_desc.get_family() 256 size = font_desc.get_size() 257 size = size / Pango.SCALE 258 weight = font_desc.get_weight() 259 style = font_desc.get_style() 260 261 widget.destroy() 262 263 _buffer = self.get_buffer() 264 265 tag_name = 'font' + font 266 if not tag_name in self.fonts_tags: 267 tag_font = _buffer.create_tag(tag_name) 268 tag_font.set_property('font', family + ' ' + str(size)) 269 self.begin_tags[tag_name] = \ 270 '<span style="font-family: ' + family + '; ' + \ 271 'font-size: ' + str(size) + 'px">' 272 self.end_tags[tag_name] = '</span>' 273 self.fonts_tags.append(tag_name) 274 275 for tag in self.fonts_tags: 276 _buffer.remove_tag_by_name(tag, start, finish) 277 278 _buffer.apply_tag_by_name(tag_name, start, finish) 279 280 if weight == Pango.Weight.BOLD: 281 _buffer.apply_tag_by_name('bold', start, finish) 282 else: 283 _buffer.remove_tag_by_name('bold', start, finish) 284 285 if style == Pango.Style.ITALIC: 286 _buffer.apply_tag_by_name('italic', start, finish) 287 else: 288 _buffer.remove_tag_by_name('italic', start, finish) 289 290 def get_xhtml(self): 291 _buffer = self.get_buffer() 292 old = _buffer.get_start_iter() 293 tags = {} 294 tags['bold'] = False 295 iter_ = _buffer.get_start_iter() 296 old = _buffer.get_start_iter() 297 text = '' 298 modified = False 299 300 def xhtml_special(text): 301 text = text.replace('<', '<') 302 text = text.replace('>', '>') 303 text = text.replace('&', '&') 304 text = text.replace('\n', '<br />') 305 return text 306 307 for tag in iter_.get_toggled_tags(True): 308 tag_name = tag.get_property('name') 309 if tag_name not in self.begin_tags: 310 continue 311 text += self.begin_tags[tag_name] 312 modified = True 313 while (iter_.forward_to_tag_toggle(None) and not iter_.is_end()): 314 text += xhtml_special(_buffer.get_text(old, iter_, True)) 315 old.forward_to_tag_toggle(None) 316 new_tags, old_tags, end_tags = [], [], [] 317 for tag in iter_.get_toggled_tags(True): 318 tag_name = tag.get_property('name') 319 if tag_name not in self.begin_tags: 320 continue 321 new_tags.append(tag_name) 322 modified = True 323 324 for tag in iter_.get_tags(): 325 tag_name = tag.get_property('name') 326 if (tag_name not in self.begin_tags or 327 tag_name not in self.end_tags): 328 continue 329 if tag_name not in new_tags: 330 old_tags.append(tag_name) 331 332 for tag in iter_.get_toggled_tags(False): 333 tag_name = tag.get_property('name') 334 if tag_name not in self.end_tags: 335 continue 336 end_tags.append(tag_name) 337 338 for tag in old_tags: 339 text += self.end_tags[tag] 340 for tag in end_tags: 341 text += self.end_tags[tag] 342 for tag in new_tags: 343 text += self.begin_tags[tag] 344 for tag in old_tags: 345 text += self.begin_tags[tag] 346 347 buffer_text = _buffer.get_text(old, _buffer.get_end_iter(), True) 348 text += xhtml_special(buffer_text) 349 for tag in iter_.get_toggled_tags(False): 350 tag_name = tag.get_property('name') 351 if tag_name not in self.end_tags: 352 continue 353 text += self.end_tags[tag_name] 354 355 if modified: 356 wrapped_text = '<p>%s</p>' % self.make_clickable_urls(text) 357 return build_xhtml_body(wrapped_text) 358 return None 359 360 def replace_emojis(self): 361 theme = app.settings.get('emoticons_theme') 362 if not theme or theme == 'font': 363 return 364 365 def replace(anchor): 366 if anchor is None: 367 return 368 image = anchor.get_widgets()[0] 369 if hasattr(image, 'codepoint'): 370 # found emoji 371 self.replace_char_at_iter(iter_, image.codepoint) 372 image.destroy() 373 374 iter_ = self.get_buffer().get_start_iter() 375 replace(iter_.get_child_anchor()) 376 377 while iter_.forward_char(): 378 replace(iter_.get_child_anchor()) 379 380 def replace_char_at_iter(self, iter_, new_char): 381 buffer_ = self.get_buffer() 382 iter_2 = iter_.copy() 383 iter_2.forward_char() 384 buffer_.delete(iter_, iter_2) 385 buffer_.insert(iter_, new_char) 386 387 def insert_emoji(self, codepoint, pixbuf): 388 buffer_ = self.get_buffer() 389 if buffer_.get_char_count(): 390 # buffer contains text 391 buffer_.insert_at_cursor(' ') 392 393 insert_mark = buffer_.get_insert() 394 insert_iter = buffer_.get_iter_at_mark(insert_mark) 395 396 if pixbuf is None: 397 buffer_.insert(insert_iter, codepoint) 398 else: 399 anchor = buffer_.create_child_anchor(insert_iter) 400 image = Gtk.Image.new_from_pixbuf(pixbuf) 401 image.codepoint = codepoint 402 image.show() 403 self.add_child_at_anchor(image, anchor) 404 buffer_.insert_at_cursor(' ') 405 406 def clear(self, _widget=None): 407 """ 408 Clear text in the textview 409 """ 410 _buffer = self.get_buffer() 411 start, end = _buffer.get_bounds() 412 _buffer.delete(start, end) 413 414 def save_undo(self, text): 415 self.undo_list.append(text) 416 if len(self.undo_list) > self.UNDO_LIMIT: 417 del self.undo_list[0] 418 self.undo_pressed = False 419 420 def undo(self, _widget=None): 421 """ 422 Undo text in the textview 423 """ 424 _buffer = self.get_buffer() 425 if self.undo_list: 426 _buffer.set_text(self.undo_list.pop()) 427 self.undo_pressed = True 428