don't mix TextTag and tag names in buffer.insert_with_tags. Fixes #5581
[gajim.git] / src / conversation_textview.py
blob4aa9812974f4e60c192a5a58a0e8f00bcff148b5
1 # -*- coding:utf-8 -*-
2 ## src/conversation_textview.py
3 ##
4 ## Copyright (C) 2005 Norman Rasmussen <norman AT rasmussen.co.za>
5 ## Copyright (C) 2005-2006 Alex Mauer <hawke AT hawkesnest.net>
6 ## Travis Shirk <travis AT pobox.com>
7 ## Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
8 ## Copyright (C) 2005-2008 Yann Leboulanger <asterix AT lagaule.org>
9 ## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
10 ## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
11 ## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
12 ## Julien Pivotto <roidelapluie AT gmail.com>
13 ## Stephan Erb <steve-e AT h3c.de>
15 ## This file is part of Gajim.
17 ## Gajim is free software; you can redistribute it and/or modify
18 ## it under the terms of the GNU General Public License as published
19 ## by the Free Software Foundation; version 3 only.
21 ## Gajim is distributed in the hope that it will be useful,
22 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
23 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 ## GNU General Public License for more details.
26 ## You should have received a copy of the GNU General Public License
27 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
30 from threading import Timer # for smooth scrolling
32 import gtk
33 import pango
34 import gobject
35 import time
36 import os
37 import tooltips
38 import dialogs
39 import locale
40 import Queue
42 import gtkgui_helpers
43 from common import gajim
44 from common import helpers
45 from common import latex
46 from calendar import timegm
47 from common.fuzzyclock import FuzzyClock
49 from htmltextview import HtmlTextView
50 from common.exceptions import GajimGeneralException
51 from common.exceptions import LatexError
53 NOT_SHOWN = 0
54 ALREADY_RECEIVED = 1
55 SHOWN = 2
57 def is_selection_modified(mark):
58 name = mark.get_name()
59 if name and name in ('selection_bound', 'insert'):
60 return True
61 else:
62 return False
64 def has_focus(widget):
65 return widget.flags() & gtk.HAS_FOCUS == gtk.HAS_FOCUS
67 class TextViewImage(gtk.Image):
69 def __init__(self, anchor, text):
70 super(TextViewImage, self).__init__()
71 self.anchor = anchor
72 self._selected = False
73 self._disconnect_funcs = []
74 self.connect('parent-set', self.on_parent_set)
75 self.connect('expose-event', self.on_expose)
76 self.set_tooltip_text(text)
77 self.anchor.set_data('plaintext', text)
79 def _get_selected(self):
80 parent = self.get_parent()
81 if not parent or not self.anchor: return False
82 buffer_ = parent.get_buffer()
83 position = buffer_.get_iter_at_child_anchor(self.anchor)
84 bounds = buffer_.get_selection_bounds()
85 if bounds and position.in_range(*bounds):
86 return True
87 else:
88 return False
90 def get_state(self):
91 parent = self.get_parent()
92 if not parent:
93 return gtk.STATE_NORMAL
94 if self._selected:
95 if has_focus(parent):
96 return gtk.STATE_SELECTED
97 else:
98 return gtk.STATE_ACTIVE
99 else:
100 return gtk.STATE_NORMAL
102 def _update_selected(self):
103 selected = self._get_selected()
104 if self._selected != selected:
105 self._selected = selected
106 self.queue_draw()
108 def _do_connect(self, widget, signal, callback):
109 id_ = widget.connect(signal, callback)
110 def disconnect():
111 widget.disconnect(id_)
112 self._disconnect_funcs.append(disconnect)
114 def _disconnect_signals(self):
115 for func in self._disconnect_funcs:
116 func()
117 self._disconnect_funcs = []
119 def on_parent_set(self, widget, old_parent):
120 parent = self.get_parent()
121 if not parent:
122 self._disconnect_signals()
123 return
125 self._do_connect(parent, 'style-set', self.do_queue_draw)
126 self._do_connect(parent, 'focus-in-event', self.do_queue_draw)
127 self._do_connect(parent, 'focus-out-event', self.do_queue_draw)
129 textbuf = parent.get_buffer()
130 self._do_connect(textbuf, 'mark-set', self.on_mark_set)
131 self._do_connect(textbuf, 'mark-deleted', self.on_mark_deleted)
133 def do_queue_draw(self, *args):
134 self.queue_draw()
135 return False
137 def on_mark_set(self, buf, iterat, mark):
138 self.on_mark_modified(mark)
139 return False
141 def on_mark_deleted(self, buf, mark):
142 self.on_mark_modified(mark)
143 return False
145 def on_mark_modified(self, mark):
146 if is_selection_modified(mark):
147 self._update_selected()
149 def on_expose(self, widget, event):
150 state = self.get_state()
151 if state != gtk.STATE_NORMAL:
152 gc = widget.get_style().base_gc[state]
153 area = widget.allocation
154 widget.window.draw_rectangle(gc, True, area.x, area.y,
155 area.width, area.height)
156 return False
159 class ConversationTextview(gobject.GObject):
160 '''Class for the conversation textview (where user reads already said
161 messages) for chat/groupchat windows'''
162 __gsignals__ = dict(
163 quote = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION,
164 None, # return value
165 (str, ) # arguments
169 FOCUS_OUT_LINE_PIXBUF = gtk.gdk.pixbuf_new_from_file(os.path.join(
170 gajim.DATA_DIR, 'pixmaps', 'muc_separator.png'))
171 XEP0184_WARNING_PIXBUF = gtk.gdk.pixbuf_new_from_file(os.path.join(
172 gajim.DATA_DIR, 'pixmaps', 'receipt_missing.png'))
174 # smooth scroll constants
175 MAX_SCROLL_TIME = 0.4 # seconds
176 SCROLL_DELAY = 33 # milliseconds
178 def __init__(self, account, used_in_history_window = False):
179 '''if used_in_history_window is True, then we do not show
180 Clear menuitem in context menu'''
181 gobject.GObject.__init__(self)
182 self.used_in_history_window = used_in_history_window
184 self.fc = FuzzyClock()
187 # no need to inherit TextView, use it as atrribute is safer
188 self.tv = HtmlTextView()
189 self.tv.html_hyperlink_handler = self.html_hyperlink_handler
191 # set properties
192 self.tv.set_border_width(1)
193 self.tv.set_accepts_tab(True)
194 self.tv.set_editable(False)
195 self.tv.set_cursor_visible(False)
196 self.tv.set_wrap_mode(gtk.WRAP_WORD_CHAR)
197 self.tv.set_left_margin(2)
198 self.tv.set_right_margin(2)
199 self.handlers = {}
200 self.images = []
201 self.image_cache = {}
202 self.xep0184_marks = {}
203 self.xep0184_shown = {}
205 # It's True when we scroll in the code, so we can detect scroll from user
206 self.auto_scrolling = False
208 # connect signals
209 id_ = self.tv.connect('motion_notify_event',
210 self.on_textview_motion_notify_event)
211 self.handlers[id_] = self.tv
212 id_ = self.tv.connect('populate_popup', self.on_textview_populate_popup)
213 self.handlers[id_] = self.tv
214 id_ = self.tv.connect('button_press_event',
215 self.on_textview_button_press_event)
216 self.handlers[id_] = self.tv
218 id_ = self.tv.connect('expose-event',
219 self.on_textview_expose_event)
220 self.handlers[id_] = self.tv
223 self.account = account
224 self.change_cursor = False
225 self.last_time_printout = 0
227 font = pango.FontDescription(gajim.config.get('conversation_font'))
228 self.tv.modify_font(font)
229 buffer_ = self.tv.get_buffer()
230 end_iter = buffer_.get_end_iter()
231 buffer_.create_mark('end', end_iter, False)
233 self.tagIn = buffer_.create_tag('incoming')
234 color = gajim.config.get('inmsgcolor')
235 font = pango.FontDescription(gajim.config.get('inmsgfont'))
236 self.tagIn.set_property('foreground', color)
237 self.tagIn.set_property('font-desc', font)
239 self.tagOut = buffer_.create_tag('outgoing')
240 color = gajim.config.get('outmsgcolor')
241 font = pango.FontDescription(gajim.config.get('outmsgfont'))
242 self.tagOut.set_property('foreground', color)
243 self.tagOut.set_property('font-desc', font)
245 self.tagStatus = buffer_.create_tag('status')
246 color = gajim.config.get('statusmsgcolor')
247 font = pango.FontDescription(gajim.config.get('satusmsgfont'))
248 self.tagStatus.set_property('foreground', color)
249 self.tagStatus.set_property('font-desc', font)
251 self.tagInText = buffer_.create_tag('incomingtxt')
252 color = gajim.config.get('inmsgtxtcolor')
253 font = pango.FontDescription(gajim.config.get('inmsgtxtfont'))
254 if color:
255 self.tagInText.set_property('foreground', color)
256 self.tagInText.set_property('font-desc', font)
258 self.tagOutText = buffer_.create_tag('outgoingtxt')
259 color = gajim.config.get('outmsgtxtcolor')
260 if color:
261 font = pango.FontDescription(gajim.config.get('outmsgtxtfont'))
262 self.tagOutText.set_property('foreground', color)
263 self.tagOutText.set_property('font-desc', font)
265 colors = gajim.config.get('gc_nicknames_colors')
266 colors = colors.split(':')
267 for i,color in enumerate(colors):
268 tagname = 'gc_nickname_color_' + str(i)
269 tag = buffer_.create_tag(tagname)
270 tag.set_property('foreground', color)
272 tag = buffer_.create_tag('marked')
273 color = gajim.config.get('markedmsgcolor')
274 tag.set_property('foreground', color)
275 tag.set_property('weight', pango.WEIGHT_BOLD)
277 tag = buffer_.create_tag('time_sometimes')
278 tag.set_property('foreground', 'darkgrey')
279 tag.set_property('scale', pango.SCALE_SMALL)
280 tag.set_property('justification', gtk.JUSTIFY_CENTER)
282 tag = buffer_.create_tag('small')
283 tag.set_property('scale', pango.SCALE_SMALL)
285 tag = buffer_.create_tag('restored_message')
286 color = gajim.config.get('restored_messages_color')
287 tag.set_property('foreground', color)
289 self.tagURL = buffer_.create_tag('url')
290 color = gajim.config.get('urlmsgcolor')
291 self.tagURL.set_property('foreground', color)
292 self.tagURL.set_property('underline', pango.UNDERLINE_SINGLE)
293 id_ = self.tagURL.connect('event', self.hyperlink_handler, 'url')
294 self.handlers[id_] = self.tagURL
296 self.tagMail = buffer_.create_tag('mail')
297 self.tagMail.set_property('foreground', color)
298 self.tagMail.set_property('underline', pango.UNDERLINE_SINGLE)
299 id_ = self.tagMail.connect('event', self.hyperlink_handler, 'mail')
300 self.handlers[id_] = self.tagMail
302 self.tagXMPP = buffer_.create_tag('xmpp')
303 self.tagXMPP.set_property('foreground', color)
304 self.tagXMPP.set_property('underline', pango.UNDERLINE_SINGLE)
305 id_ = self.tagXMPP.connect('event', self.hyperlink_handler, 'xmpp')
306 self.handlers[id_] = self.tagXMPP
308 self.tagSthAtSth = buffer_.create_tag('sth_at_sth')
309 self.tagSthAtSth.set_property('foreground', color)
310 self.tagSthAtSth.set_property('underline', pango.UNDERLINE_SINGLE)
311 id_ = self.tagSthAtSth.connect('event', self.hyperlink_handler,
312 'sth_at_sth')
313 self.handlers[id_] = self.tagSthAtSth
315 tag = buffer_.create_tag('bold')
316 tag.set_property('weight', pango.WEIGHT_BOLD)
318 tag = buffer_.create_tag('italic')
319 tag.set_property('style', pango.STYLE_ITALIC)
321 tag = buffer_.create_tag('underline')
322 tag.set_property('underline', pango.UNDERLINE_SINGLE)
324 buffer_.create_tag('focus-out-line', justification = gtk.JUSTIFY_CENTER)
326 tag = buffer_.create_tag('xep0184-warning')
328 # One mark at the begining then 2 marks between each lines
329 size = gajim.config.get('max_conversation_lines')
330 size = 2 * size - 1
331 self.marks_queue = Queue.Queue(size)
333 self.allow_focus_out_line = True
334 # holds a mark at the end of --- line
335 self.focus_out_end_mark = None
337 self.xep0184_warning_tooltip = tooltips.BaseTooltip()
339 self.line_tooltip = tooltips.BaseTooltip()
340 # use it for hr too
341 self.tv.focus_out_line_pixbuf = ConversationTextview.FOCUS_OUT_LINE_PIXBUF
342 self.smooth_id = None
344 def del_handlers(self):
345 for i in self.handlers.keys():
346 if self.handlers[i].handler_is_connected(i):
347 self.handlers[i].disconnect(i)
348 del self.handlers
349 self.tv.destroy()
350 #FIXME:
351 # self.line_tooltip.destroy()
353 def update_tags(self):
354 self.tagIn.set_property('foreground', gajim.config.get('inmsgcolor'))
355 self.tagOut.set_property('foreground', gajim.config.get('outmsgcolor'))
356 self.tagStatus.set_property('foreground',
357 gajim.config.get('statusmsgcolor'))
358 self.tagURL.set_property('foreground', gajim.config.get('urlmsgcolor'))
359 self.tagMail.set_property('foreground', gajim.config.get('urlmsgcolor'))
361 def at_the_end(self):
362 buffer_ = self.tv.get_buffer()
363 end_iter = buffer_.get_end_iter()
364 end_rect = self.tv.get_iter_location(end_iter)
365 visible_rect = self.tv.get_visible_rect()
366 if end_rect.y <= (visible_rect.y + visible_rect.height):
367 return True
368 return False
370 # Smooth scrolling inspired by Pidgin code
371 def smooth_scroll(self):
372 parent = self.tv.get_parent()
373 if not parent:
374 return False
375 vadj = parent.get_vadjustment()
376 max_val = vadj.upper - vadj.page_size + 1
377 cur_val = vadj.get_value()
378 # scroll by 1/3rd of remaining distance
379 onethird = cur_val + ((max_val - cur_val) / 3.0)
380 self.auto_scrolling = True
381 vadj.set_value(onethird)
382 self.auto_scrolling = False
383 if max_val - onethird < 0.01:
384 self.smooth_id = None
385 self.smooth_scroll_timer.cancel()
386 return False
387 return True
389 def smooth_scroll_timeout(self):
390 gobject.idle_add(self.do_smooth_scroll_timeout)
391 return
393 def do_smooth_scroll_timeout(self):
394 if not self.smooth_id:
395 # we finished scrolling
396 return
397 gobject.source_remove(self.smooth_id)
398 self.smooth_id = None
399 parent = self.tv.get_parent()
400 if parent:
401 vadj = parent.get_vadjustment()
402 self.auto_scrolling = True
403 vadj.set_value(vadj.upper - vadj.page_size + 1)
404 self.auto_scrolling = False
406 def smooth_scroll_to_end(self):
407 if None != self.smooth_id: # already scrolling
408 return False
409 self.smooth_id = gobject.timeout_add(self.SCROLL_DELAY,
410 self.smooth_scroll)
411 self.smooth_scroll_timer = Timer(self.MAX_SCROLL_TIME,
412 self.smooth_scroll_timeout)
413 self.smooth_scroll_timer.start()
414 return False
416 def scroll_to_end(self):
417 parent = self.tv.get_parent()
418 buffer_ = self.tv.get_buffer()
419 end_mark = buffer_.get_mark('end')
420 if not end_mark:
421 return False
422 self.auto_scrolling = True
423 self.tv.scroll_to_mark(end_mark, 0, True, 0, 1)
424 adjustment = parent.get_hadjustment()
425 adjustment.set_value(0)
426 self.auto_scrolling = False
427 return False # when called in an idle_add, just do it once
429 def bring_scroll_to_end(self, diff_y = 0,
430 use_smooth=gajim.config.get('use_smooth_scrolling')):
431 ''' scrolls to the end of textview if end is not visible '''
432 buffer_ = self.tv.get_buffer()
433 end_iter = buffer_.get_end_iter()
434 end_rect = self.tv.get_iter_location(end_iter)
435 visible_rect = self.tv.get_visible_rect()
436 # scroll only if expected end is not visible
437 if end_rect.y >= (visible_rect.y + visible_rect.height + diff_y):
438 if use_smooth:
439 gobject.idle_add(self.smooth_scroll_to_end)
440 else:
441 gobject.idle_add(self.scroll_to_end_iter)
443 def scroll_to_end_iter(self):
444 buffer_ = self.tv.get_buffer()
445 end_iter = buffer_.get_end_iter()
446 if not end_iter:
447 return False
448 self.tv.scroll_to_iter(end_iter, 0, False, 1, 1)
449 return False # when called in an idle_add, just do it once
451 def stop_scrolling(self):
452 if self.smooth_id:
453 gobject.source_remove(self.smooth_id)
454 self.smooth_id = None
455 self.smooth_scroll_timer.cancel()
457 def show_xep0184_warning(self, id_):
458 if id_ in self.xep0184_marks:
459 return
461 buffer_ = self.tv.get_buffer()
462 buffer_.begin_user_action()
464 self.xep0184_marks[id_] = buffer_.create_mark(None,
465 buffer_.get_end_iter(), left_gravity=True)
466 self.xep0184_shown[id_] = NOT_SHOWN
468 def show_it():
469 if (not id_ in self.xep0184_shown) or \
470 self.xep0184_shown[id_] == ALREADY_RECEIVED:
471 return False
473 end_iter = buffer_.get_iter_at_mark(self.xep0184_marks[id_])
474 buffer_.insert(end_iter, ' ')
475 anchor = buffer_.create_child_anchor(end_iter)
476 img = TextViewImage(anchor, '')
477 img.set_from_pixbuf(ConversationTextview.XEP0184_WARNING_PIXBUF)
478 img.show()
479 self.tv.add_child_at_anchor(img, anchor)
480 before_img_iter = buffer_.get_iter_at_mark(self.xep0184_marks[id_])
481 before_img_iter.forward_char()
482 post_img_iter = before_img_iter.copy()
483 post_img_iter.forward_char()
484 buffer_.apply_tag_by_name('xep0184-warning', before_img_iter,
485 post_img_iter)
487 self.xep0184_shown[id_] = SHOWN
488 return False
489 gobject.timeout_add_seconds(3, show_it)
491 buffer_.end_user_action()
493 def hide_xep0184_warning(self, id_):
494 if id_ not in self.xep0184_marks:
495 return
497 if self.xep0184_shown[id_] == NOT_SHOWN:
498 self.xep0184_shown[id_] = ALREADY_RECEIVED
499 return
501 buffer_ = self.tv.get_buffer()
502 buffer_.begin_user_action()
504 begin_iter = buffer_.get_iter_at_mark(self.xep0184_marks[id_])
506 end_iter = begin_iter.copy()
507 # XXX: Is there a nicer way?
508 end_iter.forward_char()
509 end_iter.forward_char()
511 buffer_.delete(begin_iter, end_iter)
512 buffer_.delete_mark(self.xep0184_marks[id_])
514 buffer_.end_user_action()
516 del self.xep0184_marks[id_]
517 del self.xep0184_shown[id_]
519 def show_focus_out_line(self):
520 if not self.allow_focus_out_line:
521 # if room did not receive focus-in from the last time we added
522 # --- line then do not readd
523 return
525 print_focus_out_line = False
526 buffer_ = self.tv.get_buffer()
528 if self.focus_out_end_mark is None:
529 # this happens only first time we focus out on this room
530 print_focus_out_line = True
532 else:
533 focus_out_end_iter = buffer_.get_iter_at_mark(self.focus_out_end_mark)
534 focus_out_end_iter_offset = focus_out_end_iter.get_offset()
535 if focus_out_end_iter_offset != buffer_.get_end_iter().get_offset():
536 # this means after last-focus something was printed
537 # (else end_iter's offset is the same as before)
538 # only then print ---- line (eg. we avoid printing many following
539 # ---- lines)
540 print_focus_out_line = True
542 if print_focus_out_line and buffer_.get_char_count() > 0:
543 buffer_.begin_user_action()
545 # remove previous focus out line if such focus out line exists
546 if self.focus_out_end_mark is not None:
547 end_iter_for_previous_line = buffer_.get_iter_at_mark(
548 self.focus_out_end_mark)
549 begin_iter_for_previous_line = end_iter_for_previous_line.copy()
550 # img_char+1 (the '\n')
551 begin_iter_for_previous_line.backward_chars(2)
553 # remove focus out line
554 buffer_.delete(begin_iter_for_previous_line,
555 end_iter_for_previous_line)
556 buffer_.delete_mark(self.focus_out_end_mark)
558 # add the new focus out line
559 end_iter = buffer_.get_end_iter()
560 buffer_.insert(end_iter, '\n')
561 buffer_.insert_pixbuf(end_iter,
562 ConversationTextview.FOCUS_OUT_LINE_PIXBUF)
564 end_iter = buffer_.get_end_iter()
565 before_img_iter = end_iter.copy()
566 # one char back (an image also takes one char)
567 before_img_iter.backward_char()
568 buffer_.apply_tag_by_name('focus-out-line', before_img_iter, end_iter)
570 self.allow_focus_out_line = False
572 # update the iter we hold to make comparison the next time
573 self.focus_out_end_mark = buffer_.create_mark(None,
574 buffer_.get_end_iter(), left_gravity=True)
576 buffer_.end_user_action()
578 # scroll to the end (via idle in case the scrollbar has appeared)
579 gobject.idle_add(self.scroll_to_end)
581 def show_xep0184_warning_tooltip(self):
582 pointer = self.tv.get_pointer()
583 x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
584 pointer[0], pointer[1])
585 tags = self.tv.get_iter_at_location(x, y).get_tags()
586 tag_table = self.tv.get_buffer().get_tag_table()
587 xep0184_warning = False
588 for tag in tags:
589 if tag == tag_table.lookup('xep0184-warning'):
590 xep0184_warning = True
591 break
592 if xep0184_warning and not self.xep0184_warning_tooltip.win:
593 # check if the current pointer is still over the line
594 position = self.tv.window.get_origin()
595 self.xep0184_warning_tooltip.show_tooltip(_('This icon indicates that '
596 'this message has not yet\nbeen received by the remote end. '
597 "If this icon stays\nfor a long time, it's likely the message got "
598 'lost.'), 8, position[1] + pointer[1])
600 def show_line_tooltip(self):
601 pointer = self.tv.get_pointer()
602 x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
603 pointer[0], pointer[1])
604 tags = self.tv.get_iter_at_location(x, y).get_tags()
605 tag_table = self.tv.get_buffer().get_tag_table()
606 over_line = False
607 for tag in tags:
608 if tag == tag_table.lookup('focus-out-line'):
609 over_line = True
610 break
611 if over_line and not self.line_tooltip.win:
612 # check if the current pointer is still over the line
613 position = self.tv.window.get_origin()
614 self.line_tooltip.show_tooltip(_('Text below this line is what has '
615 'been said since the\nlast time you paid attention to this group '
616 'chat'), 8, position[1] + pointer[1])
618 def on_textview_expose_event(self, widget, event):
619 expalloc = event.area
620 exp_x0 = expalloc.x
621 exp_y0 = expalloc.y
622 exp_x1 = exp_x0 + expalloc.width
623 exp_y1 = exp_y0 + expalloc.height
625 try:
626 tryfirst = [self.image_cache[(exp_x0, exp_y0)]]
627 except KeyError:
628 tryfirst = []
630 for image in tryfirst + self.images:
631 imgalloc = image.allocation
632 img_x0 = imgalloc.x
633 img_y0 = imgalloc.y
634 img_x1 = img_x0 + imgalloc.width
635 img_y1 = img_y0 + imgalloc.height
637 if img_x0 <= exp_x0 and img_y0 <= exp_y0 and \
638 exp_x1 <= img_x1 and exp_y1 <= img_y1:
639 self.image_cache[(img_x0, img_y0)] = image
640 widget.propagate_expose(image, event)
641 return True
642 return False
644 def on_textview_motion_notify_event(self, widget, event):
645 '''change the cursor to a hand when we are over a mail or an
646 url'''
647 pointer_x, pointer_y = self.tv.window.get_pointer()[0:2]
648 x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
649 pointer_x, pointer_y)
650 tags = self.tv.get_iter_at_location(x, y).get_tags()
651 if self.change_cursor:
652 self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
653 gtk.gdk.Cursor(gtk.gdk.XTERM))
654 self.change_cursor = False
655 tag_table = self.tv.get_buffer().get_tag_table()
656 over_line = False
657 xep0184_warning = False
658 for tag in tags:
659 if tag in (tag_table.lookup('url'), tag_table.lookup('mail'), \
660 tag_table.lookup('xmpp'), tag_table.lookup('sth_at_sth')):
661 self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
662 gtk.gdk.Cursor(gtk.gdk.HAND2))
663 self.change_cursor = True
664 elif tag == tag_table.lookup('focus-out-line'):
665 over_line = True
666 elif tag == tag_table.lookup('xep0184-warning'):
667 xep0184_warning = True
669 if self.line_tooltip.timeout != 0:
670 # Check if we should hide the line tooltip
671 if not over_line:
672 self.line_tooltip.hide_tooltip()
673 if self.xep0184_warning_tooltip.timeout != 0:
674 # Check if we should hide the XEP-184 warning tooltip
675 if not xep0184_warning:
676 self.xep0184_warning_tooltip.hide_tooltip()
677 if over_line and not self.line_tooltip.win:
678 self.line_tooltip.timeout = gobject.timeout_add(500,
679 self.show_line_tooltip)
680 self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
681 gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
682 self.change_cursor = True
683 if xep0184_warning and not self.xep0184_warning_tooltip.win:
684 self.xep0184_warning_tooltip.timeout = gobject.timeout_add(500,
685 self.show_xep0184_warning_tooltip)
686 self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
687 gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
688 self.change_cursor = True
690 def clear(self, tv = None):
691 '''clear text in the textview'''
692 buffer_ = self.tv.get_buffer()
693 start, end = buffer_.get_bounds()
694 buffer_.delete(start, end)
695 size = gajim.config.get('max_conversation_lines')
696 size = 2 * size - 1
697 self.marks_queue = Queue.Queue(size)
698 self.focus_out_end_mark = None
700 def visit_url_from_menuitem(self, widget, link):
701 '''basically it filters out the widget instance'''
702 helpers.launch_browser_mailer('url', link)
704 def on_textview_populate_popup(self, textview, menu):
705 '''we override the default context menu and we prepend Clear
706 (only if used_in_history_window is False)
707 and if we have sth selected we show a submenu with actions on
708 the phrase (see on_conversation_textview_button_press_event)'''
710 separator_menuitem_was_added = False
711 if not self.used_in_history_window:
712 item = gtk.SeparatorMenuItem()
713 menu.prepend(item)
714 separator_menuitem_was_added = True
716 item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
717 menu.prepend(item)
718 id_ = item.connect('activate', self.clear)
719 self.handlers[id_] = item
721 if self.selected_phrase:
722 if not separator_menuitem_was_added:
723 item = gtk.SeparatorMenuItem()
724 menu.prepend(item)
726 if not self.used_in_history_window:
727 item = gtk.MenuItem(_('_Quote'))
728 id_ = item.connect('activate', self.on_quote)
729 self.handlers[id_] = item
730 menu.prepend(item)
732 _selected_phrase = helpers.reduce_chars_newlines(
733 self.selected_phrase, 25, 2)
734 item = gtk.MenuItem(_('_Actions for "%s"') % _selected_phrase)
735 menu.prepend(item)
736 submenu = gtk.Menu()
737 item.set_submenu(submenu)
739 always_use_en = gajim.config.get('always_english_wikipedia')
740 if always_use_en:
741 link = 'http://en.wikipedia.org/wiki/Special:Search?search=%s'\
742 % self.selected_phrase
743 else:
744 link = 'http://%s.wikipedia.org/wiki/Special:Search?search=%s'\
745 % (gajim.LANG, self.selected_phrase)
746 item = gtk.MenuItem(_('Read _Wikipedia Article'))
747 id_ = item.connect('activate', self.visit_url_from_menuitem, link)
748 self.handlers[id_] = item
749 submenu.append(item)
751 item = gtk.MenuItem(_('Look it up in _Dictionary'))
752 dict_link = gajim.config.get('dictionary_url')
753 if dict_link == 'WIKTIONARY':
754 # special link (yeah undocumented but default)
755 always_use_en = gajim.config.get('always_english_wiktionary')
756 if always_use_en:
757 link = 'http://en.wiktionary.org/wiki/Special:Search?search=%s'\
758 % self.selected_phrase
759 else:
760 link = 'http://%s.wiktionary.org/wiki/Special:Search?search=%s'\
761 % (gajim.LANG, self.selected_phrase)
762 id_ = item.connect('activate', self.visit_url_from_menuitem, link)
763 self.handlers[id_] = item
764 else:
765 if dict_link.find('%s') == -1:
766 # we must have %s in the url if not WIKTIONARY
767 item = gtk.MenuItem(_(
768 'Dictionary URL is missing an "%s" and it is not WIKTIONARY'))
769 item.set_property('sensitive', False)
770 else:
771 link = dict_link % self.selected_phrase
772 id_ = item.connect('activate', self.visit_url_from_menuitem,
773 link)
774 self.handlers[id_] = item
775 submenu.append(item)
778 search_link = gajim.config.get('search_engine')
779 if search_link.find('%s') == -1:
780 # we must have %s in the url
781 item = gtk.MenuItem(_('Web Search URL is missing an "%s"'))
782 item.set_property('sensitive', False)
783 else:
784 item = gtk.MenuItem(_('Web _Search for it'))
785 link = search_link % self.selected_phrase
786 id_ = item.connect('activate', self.visit_url_from_menuitem, link)
787 self.handlers[id_] = item
788 submenu.append(item)
790 item = gtk.MenuItem(_('Open as _Link'))
791 id_ = item.connect('activate', self.visit_url_from_menuitem, link)
792 self.handlers[id_] = item
793 submenu.append(item)
795 menu.show_all()
797 def on_quote(self, widget):
798 self.emit('quote', self.selected_phrase)
800 def on_textview_button_press_event(self, widget, event):
801 # If we clicked on a taged text do NOT open the standard popup menu
802 # if normal text check if we have sth selected
803 self.selected_phrase = '' # do not move belove event button check!
805 if event.button != 3: # if not right click
806 return False
808 x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
809 int(event.x), int(event.y))
810 iter_ = self.tv.get_iter_at_location(x, y)
811 tags = iter_.get_tags()
814 if tags: # we clicked on sth special (it can be status message too)
815 for tag in tags:
816 tag_name = tag.get_property('name')
817 if tag_name in ('url', 'mail', 'xmpp', 'sth_at_sth'):
818 return True # we block normal context menu
820 # we check if sth was selected and if it was we assign
821 # selected_phrase variable
822 # so on_conversation_textview_populate_popup can use it
823 buffer_ = self.tv.get_buffer()
824 return_val = buffer_.get_selection_bounds()
825 if return_val: # if sth was selected when we right-clicked
826 # get the selected text
827 start_sel, finish_sel = return_val[0], return_val[1]
828 self.selected_phrase = buffer_.get_text(start_sel, finish_sel).decode(
829 'utf-8')
830 elif ord(iter_.get_char()) > 31:
831 # we clicked on a word, do as if it's selected for context menu
832 start_sel = iter_.copy()
833 if not start_sel.starts_word():
834 start_sel.backward_word_start()
835 finish_sel = iter_.copy()
836 if not finish_sel.ends_word():
837 finish_sel.forward_word_end()
838 self.selected_phrase = buffer_.get_text(start_sel, finish_sel).decode(
839 'utf-8')
841 def on_open_link_activate(self, widget, kind, text):
842 helpers.launch_browser_mailer(kind, text)
844 def on_copy_link_activate(self, widget, text):
845 clip = gtk.clipboard_get()
846 clip.set_text(text)
848 def on_start_chat_activate(self, widget, jid):
849 gajim.interface.new_chat_from_jid(self.account, jid)
851 def on_join_group_chat_menuitem_activate(self, widget, room_jid):
852 if 'join_gc' in gajim.interface.instances[self.account]:
853 instance = gajim.interface.instances[self.account]['join_gc']
854 instance.xml.get_widget('room_jid_entry').set_text(room_jid)
855 gajim.interface.instances[self.account]['join_gc'].window.present()
856 else:
857 try:
858 dialogs.JoinGroupchatWindow(account=None, room_jid=room_jid)
859 except GajimGeneralException:
860 pass
862 def on_add_to_roster_activate(self, widget, jid):
863 dialogs.AddNewContactWindow(self.account, jid)
865 def make_link_menu(self, event, kind, text):
866 xml = gtkgui_helpers.get_glade('chat_context_menu.glade')
867 menu = xml.get_widget('chat_context_menu')
868 childs = menu.get_children()
869 if kind == 'url':
870 id_ = childs[0].connect('activate', self.on_copy_link_activate, text)
871 self.handlers[id_] = childs[0]
872 id_ = childs[1].connect('activate', self.on_open_link_activate, kind,
873 text)
874 self.handlers[id_] = childs[1]
875 childs[2].hide() # copy mail address
876 childs[3].hide() # open mail composer
877 childs[4].hide() # jid section separator
878 childs[5].hide() # start chat
879 childs[6].hide() # join group chat
880 childs[7].hide() # add to roster
881 else: # It's a mail or a JID
882 # load muc icon
883 join_group_chat_menuitem = xml.get_widget('join_group_chat_menuitem')
884 muc_icon = gtkgui_helpers.load_icon('muc_active')
885 if muc_icon:
886 join_group_chat_menuitem.set_image(muc_icon)
888 text = text.lower()
889 if text.startswith('xmpp:'):
890 text = text[5:]
891 id_ = childs[2].connect('activate', self.on_copy_link_activate, text)
892 self.handlers[id_] = childs[2]
893 id_ = childs[3].connect('activate', self.on_open_link_activate, kind,
894 text)
895 self.handlers[id_] = childs[3]
896 id_ = childs[5].connect('activate', self.on_start_chat_activate, text)
897 self.handlers[id_] = childs[5]
898 id_ = childs[6].connect('activate',
899 self.on_join_group_chat_menuitem_activate, text)
900 self.handlers[id_] = childs[6]
902 allow_add = False
903 if self.account:
904 c = gajim.contacts.get_first_contact_from_jid(self.account, text)
905 if c and not gajim.contacts.is_pm_from_contact(self.account, c):
906 if _('Not in Roster') in c.groups:
907 allow_add = True
908 else: # he or she's not at all in the account contacts
909 allow_add = True
911 if allow_add:
912 id_ = childs[7].connect('activate', self.on_add_to_roster_activate,
913 text)
914 self.handlers[id_] = childs[7]
915 childs[7].show() # show add to roster menuitem
916 else:
917 childs[7].hide() # hide add to roster menuitem
919 if kind == 'xmpp':
920 childs[2].hide() # copy mail address
921 childs[3].hide() # open mail composer
922 childs[4].hide() # jid section separator
923 elif kind == 'mail':
924 childs[4].hide() # jid section separator
925 childs[5].hide() # start chat
926 childs[6].hide() # join group chat
927 childs[7].hide() # add to roster
929 childs[0].hide() # copy link location
930 childs[1].hide() # open link in browser
932 menu.popup(None, None, None, event.button, event.time)
934 def hyperlink_handler(self, texttag, widget, event, iter_, kind):
935 if event.type == gtk.gdk.BUTTON_PRESS:
936 begin_iter = iter_.copy()
937 # we get the begining of the tag
938 while not begin_iter.begins_tag(texttag):
939 begin_iter.backward_char()
940 end_iter = iter_.copy()
941 # we get the end of the tag
942 while not end_iter.ends_tag(texttag):
943 end_iter.forward_char()
944 word = self.tv.get_buffer().get_text(begin_iter, end_iter).decode(
945 'utf-8')
946 if event.button == 3: # right click
947 self.make_link_menu(event, kind, word)
948 else:
949 # we launch the correct application
950 if kind == 'xmpp':
951 word = word[5:]
952 if '?' in word:
953 (jid, action) = word.split('?')
954 if action == 'join':
955 self.on_join_group_chat_menuitem_activate(None, jid)
956 else:
957 self.on_start_chat_activate(None, jid)
958 else:
959 self.on_start_chat_activate(None, word)
960 else:
961 helpers.launch_browser_mailer(kind, word)
963 def html_hyperlink_handler(self, texttag, widget, event, iter_, kind, href):
964 if event.type == gtk.gdk.BUTTON_PRESS:
965 if event.button == 3: # right click
966 self.make_link_menu(event, kind, href)
967 return True
968 else:
969 # we launch the correct application
970 helpers.launch_browser_mailer(kind, href)
973 def detect_and_print_special_text(self, otext, other_tags, graphics=True):
974 '''detects special text (emots & links & formatting)
975 prints normal text before any special text it founts,
976 then print special text (that happens many times until
977 last special text is printed) and then returns the index
978 after *last* special text, so we can print it in
979 print_conversation_line()'''
981 buffer_ = self.tv.get_buffer()
983 insert_tags_func = buffer_.insert_with_tags_by_name
984 # detect_and_print_special_text() is also used by
985 # HtmlHandler.handle_specials() and there tags is gtk.TextTag objects,
986 # not strings
987 if other_tags and isinstance(other_tags[0], gtk.TextTag):
988 insert_tags_func = buffer_.insert_with_tags
990 index = 0
992 # Too many special elements (emoticons, LaTeX formulas, etc)
993 # may cause Gajim to freeze (see #5129).
994 # We impose an arbitrary limit of 100 specials per message.
995 specials_limit = 100
997 # basic: links + mail + formatting is always checked (we like that)
998 if gajim.config.get('emoticons_theme') and graphics:
999 # search for emoticons & urls
1000 iterator = gajim.interface.emot_and_basic_re.finditer(otext)
1001 else: # search for just urls + mail + formatting
1002 iterator = gajim.interface.basic_pattern_re.finditer(otext)
1003 for match in iterator:
1004 start, end = match.span()
1005 special_text = otext[start:end]
1006 if start > index:
1007 text_before_special_text = otext[index:start]
1008 end_iter = buffer_.get_end_iter()
1009 # we insert normal text
1010 insert_tags_func(end_iter, text_before_special_text, *other_tags)
1011 index = end # update index
1013 # now print it
1014 self.print_special_text(special_text, other_tags, graphics=graphics)
1015 specials_limit -= 1
1016 if specials_limit <= 0:
1017 break
1019 # add the rest of text located in the index and after
1020 end_iter = buffer_.get_end_iter()
1021 insert_tags_func(end_iter, otext[index:], *other_tags)
1023 return buffer_.get_end_iter()
1025 def print_special_text(self, special_text, other_tags, graphics=True):
1026 '''is called by detect_and_print_special_text and prints
1027 special text (emots, links, formatting)'''
1028 tags = []
1029 use_other_tags = True
1030 text_is_valid_uri = False
1031 show_ascii_formatting_chars = \
1032 gajim.config.get('show_ascii_formatting_chars')
1033 buffer_ = self.tv.get_buffer()
1035 # Check if we accept this as an uri
1036 schemes = gajim.config.get('uri_schemes').split()
1037 for scheme in schemes:
1038 if special_text.startswith(scheme + ':'):
1039 text_is_valid_uri = True
1041 possible_emot_ascii_caps = special_text.upper() # emoticons keys are CAPS
1042 if gajim.config.get('emoticons_theme') and \
1043 possible_emot_ascii_caps in gajim.interface.emoticons.keys() and graphics:
1044 # it's an emoticon
1045 emot_ascii = possible_emot_ascii_caps
1046 end_iter = buffer_.get_end_iter()
1047 anchor = buffer_.create_child_anchor(end_iter)
1048 img = TextViewImage(anchor, special_text)
1049 animations = gajim.interface.emoticons_animations
1050 if not emot_ascii in animations:
1051 animations[emot_ascii] = gtk.gdk.PixbufAnimation(
1052 gajim.interface.emoticons[emot_ascii])
1053 img.set_from_animation(animations[emot_ascii])
1054 img.show()
1055 self.images.append(img)
1056 # add with possible animation
1057 self.tv.add_child_at_anchor(img, anchor)
1058 elif special_text.startswith('www.') or \
1059 special_text.startswith('ftp.') or \
1060 text_is_valid_uri:
1061 tags.append('url')
1062 use_other_tags = False
1063 elif special_text.startswith('mailto:'):
1064 tags.append('mail')
1065 use_other_tags = False
1066 elif special_text.startswith('xmpp:'):
1067 tags.append('xmpp')
1068 use_other_tags = False
1069 elif gajim.interface.sth_at_sth_dot_sth_re.match(special_text):
1070 # it's a JID or mail
1071 tags.append('sth_at_sth')
1072 use_other_tags = False
1073 elif special_text.startswith('*'): # it's a bold text
1074 tags.append('bold')
1075 if special_text[1] == '/' and special_text[-2] == '/' and\
1076 len(special_text) > 4: # it's also italic
1077 tags.append('italic')
1078 if not show_ascii_formatting_chars:
1079 special_text = special_text[2:-2] # remove */ /*
1080 elif special_text[1] == '_' and special_text[-2] == '_' and \
1081 len(special_text) > 4: # it's also underlined
1082 tags.append('underline')
1083 if not show_ascii_formatting_chars:
1084 special_text = special_text[2:-2] # remove *_ _*
1085 else:
1086 if not show_ascii_formatting_chars:
1087 special_text = special_text[1:-1] # remove * *
1088 elif special_text.startswith('/'): # it's an italic text
1089 tags.append('italic')
1090 if special_text[1] == '*' and special_text[-2] == '*' and \
1091 len(special_text) > 4: # it's also bold
1092 tags.append('bold')
1093 if not show_ascii_formatting_chars:
1094 special_text = special_text[2:-2] # remove /* */
1095 elif special_text[1] == '_' and special_text[-2] == '_' and \
1096 len(special_text) > 4: # it's also underlined
1097 tags.append('underline')
1098 if not show_ascii_formatting_chars:
1099 special_text = special_text[2:-2] # remove /_ _/
1100 else:
1101 if not show_ascii_formatting_chars:
1102 special_text = special_text[1:-1] # remove / /
1103 elif special_text.startswith('_'): # it's an underlined text
1104 tags.append('underline')
1105 if special_text[1] == '*' and special_text[-2] == '*' and \
1106 len(special_text) > 4: # it's also bold
1107 tags.append('bold')
1108 if not show_ascii_formatting_chars:
1109 special_text = special_text[2:-2] # remove _* *_
1110 elif special_text[1] == '/' and special_text[-2] == '/' and \
1111 len(special_text) > 4: # it's also italic
1112 tags.append('italic')
1113 if not show_ascii_formatting_chars:
1114 special_text = special_text[2:-2] # remove _/ /_
1115 else:
1116 if not show_ascii_formatting_chars:
1117 special_text = special_text[1:-1] # remove _ _
1118 elif gajim.HAVE_LATEX and special_text.startswith('$$') and \
1119 special_text.endswith('$$') and graphics:
1120 try:
1121 imagepath = latex.latex_to_image(special_text[2:-2])
1122 except LatexError, e:
1123 # print the error after the line has been written
1124 gobject.idle_add(self.print_conversation_line, str(e), '', 'info',
1125 '', None)
1126 imagepath = None
1127 end_iter = buffer_.get_end_iter()
1128 if imagepath is not None:
1129 anchor = buffer_.create_child_anchor(end_iter)
1130 img = TextViewImage(anchor, special_text)
1131 img.set_from_file(imagepath)
1132 img.show()
1133 # add
1134 self.tv.add_child_at_anchor(img, anchor)
1135 # delete old file
1136 try:
1137 os.remove(imagepath)
1138 except Exception:
1139 pass
1140 else:
1141 buffer_.insert(end_iter, special_text)
1142 use_other_tags = False
1143 else:
1144 # It's nothing special
1145 if use_other_tags:
1146 end_iter = buffer_.get_end_iter()
1147 insert_tags_func = buffer_.insert_with_tags_by_name
1148 if other_tags and isinstance(other_tags[0], gtk.TextTag):
1149 insert_tags_func = buffer_.insert_with_tags
1151 insert_tags_func(end_iter, special_text, *other_tags)
1153 if tags:
1154 end_iter = buffer_.get_end_iter()
1155 all_tags = tags[:]
1156 if use_other_tags:
1157 all_tags += other_tags
1158 # convert all names to TextTag
1159 ttt = buffer_.get_tag_table()
1160 all_tags = [(ttt.lookup(t) if isinstance(t, str) else t) for t in all_tags]
1161 buffer_.insert_with_tags(end_iter, special_text, *all_tags)
1163 def print_empty_line(self):
1164 buffer_ = self.tv.get_buffer()
1165 end_iter = buffer_.get_end_iter()
1166 buffer_.insert_with_tags_by_name(end_iter, '\n', 'eol')
1168 def print_conversation_line(self, text, jid, kind, name, tim,
1169 other_tags_for_name=[], other_tags_for_time=[], other_tags_for_text=[],
1170 subject=None, old_kind=None, xhtml=None, simple=False, graphics=True):
1171 '''prints 'chat' type messages'''
1172 buffer_ = self.tv.get_buffer()
1173 buffer_.begin_user_action()
1174 if self.marks_queue.full():
1175 # remove oldest line
1176 m1 = self.marks_queue.get()
1177 m2 = self.marks_queue.get()
1178 i1 = buffer_.get_iter_at_mark(m1)
1179 i2 = buffer_.get_iter_at_mark(m2)
1180 buffer_.delete(i1, i2)
1181 buffer_.delete_mark(m1)
1182 end_iter = buffer_.get_end_iter()
1183 end_offset = end_iter.get_offset()
1184 at_the_end = self.at_the_end()
1185 move_selection = False
1186 if buffer_.get_has_selection() and buffer_.get_selection_bounds()[1].\
1187 get_offset() == end_offset:
1188 move_selection = True
1190 # Create one mark and add it to queue once if it's the first line
1191 # else twice (one for end bound, one for start bound)
1192 mark = None
1193 if buffer_.get_char_count() > 0:
1194 if not simple:
1195 buffer_.insert_with_tags_by_name(end_iter, '\n', 'eol')
1196 if move_selection:
1197 sel_start, sel_end = buffer_.get_selection_bounds()
1198 sel_end.backward_char()
1199 buffer_.select_range(sel_start, sel_end)
1200 mark = buffer_.create_mark(None, end_iter, left_gravity=True)
1201 self.marks_queue.put(mark)
1202 if not mark:
1203 mark = buffer_.create_mark(None, end_iter, left_gravity=True)
1204 self.marks_queue.put(mark)
1205 if kind == 'incoming_queue':
1206 kind = 'incoming'
1207 if old_kind == 'incoming_queue':
1208 old_kind = 'incoming'
1209 # print the time stamp
1210 if not tim:
1211 # We don't have tim for outgoing messages...
1212 tim = time.localtime()
1213 current_print_time = gajim.config.get('print_time')
1214 if current_print_time == 'always' and kind != 'info' and not simple:
1215 timestamp_str = self.get_time_to_show(tim)
1216 timestamp = time.strftime(timestamp_str, tim)
1217 buffer_.insert_with_tags_by_name(end_iter, timestamp,
1218 *other_tags_for_time)
1219 elif current_print_time == 'sometimes' and kind != 'info' and not simple:
1220 every_foo_seconds = 60 * gajim.config.get(
1221 'print_ichat_every_foo_minutes')
1222 seconds_passed = time.mktime(tim) - self.last_time_printout
1223 if seconds_passed > every_foo_seconds:
1224 self.last_time_printout = time.mktime(tim)
1225 end_iter = buffer_.get_end_iter()
1226 if gajim.config.get('print_time_fuzzy') > 0:
1227 ft = self.fc.fuzzy_time(gajim.config.get('print_time_fuzzy'), tim)
1228 tim_format = ft.decode(locale.getpreferredencoding())
1229 else:
1230 tim_format = self.get_time_to_show(tim)
1231 buffer_.insert_with_tags_by_name(end_iter, tim_format + '\n',
1232 'time_sometimes')
1233 # kind = info, we print things as if it was a status: same color, ...
1234 if kind in ('error', 'info'):
1235 kind = 'status'
1236 other_text_tag = self.detect_other_text_tag(text, kind)
1237 text_tags = other_tags_for_text[:] # create a new list
1238 if other_text_tag:
1239 # note that color of /me may be overwritten in gc_control
1240 text_tags.append(other_text_tag)
1241 else: # not status nor /me
1242 if gajim.config.get('chat_merge_consecutive_nickname'):
1243 if kind != old_kind:
1244 self.print_name(name, kind, other_tags_for_name)
1245 else:
1246 self.print_real_text(gajim.config.get(
1247 'chat_merge_consecutive_nickname_indent'))
1248 else:
1249 self.print_name(name, kind, other_tags_for_name)
1250 if kind == 'incoming':
1251 text_tags.append('incomingtxt')
1252 elif kind == 'outgoing':
1253 text_tags.append('outgoingtxt')
1254 self.print_subject(subject)
1255 self.print_real_text(text, text_tags, name, xhtml, graphics=graphics)
1257 # scroll to the end of the textview
1258 if at_the_end or kind == 'outgoing':
1259 # we are at the end or we are sending something
1260 # scroll to the end (via idle in case the scrollbar has appeared)
1261 if gajim.config.get('use_smooth_scrolling'):
1262 gobject.idle_add(self.smooth_scroll_to_end)
1263 else:
1264 gobject.idle_add(self.scroll_to_end)
1266 buffer_.end_user_action()
1268 def get_time_to_show(self, tim):
1269 '''Get the time, with the day before if needed and return it.
1270 It DOESN'T format a fuzzy time'''
1271 format = ''
1272 # get difference in days since epoch (86400 = 24*3600)
1273 # number of days since epoch for current time (in GMT) -
1274 # number of days since epoch for message (in GMT)
1275 diff_day = int(timegm(time.localtime())) / 86400 -\
1276 int(timegm(tim)) / 86400
1277 if diff_day == 0:
1278 day_str = ''
1279 elif diff_day == 1:
1280 day_str = _('Yesterday')
1281 else:
1282 #the number is >= 2
1283 # %i is day in year (1-365), %d (1-31) we want %i
1284 day_str = _('%i days ago') % diff_day
1285 if day_str:
1286 format += day_str + ' '
1287 timestamp_str = gajim.config.get('time_stamp')
1288 timestamp_str = helpers.from_one_line(timestamp_str)
1289 format += timestamp_str
1290 tim_format = time.strftime(format, tim)
1291 if locale.getpreferredencoding() != 'KOI8-R':
1292 # if tim_format comes as unicode because of day_str.
1293 # we convert it to the encoding that we want (and that is utf-8)
1294 tim_format = helpers.ensure_utf8_string(tim_format)
1295 return tim_format
1297 def detect_other_text_tag(self, text, kind):
1298 if kind == 'status':
1299 return kind
1300 elif text.startswith('/me ') or text.startswith('/me\n'):
1301 return kind
1303 def print_name(self, name, kind, other_tags_for_name):
1304 if name:
1305 buffer_ = self.tv.get_buffer()
1306 end_iter = buffer_.get_end_iter()
1307 name_tags = other_tags_for_name[:] # create a new list
1308 name_tags.append(kind)
1309 before_str = gajim.config.get('before_nickname')
1310 before_str = helpers.from_one_line(before_str)
1311 after_str = gajim.config.get('after_nickname')
1312 after_str = helpers.from_one_line(after_str)
1313 format = before_str + name + after_str + ' '
1314 buffer_.insert_with_tags_by_name(end_iter, format, *name_tags)
1316 def print_subject(self, subject):
1317 if subject: # if we have subject, show it too!
1318 subject = _('Subject: %s\n') % subject
1319 buffer_ = self.tv.get_buffer()
1320 end_iter = buffer_.get_end_iter()
1321 buffer_.insert(end_iter, subject)
1322 self.print_empty_line()
1324 def print_real_text(self, text, text_tags=[], name=None, xhtml=None,
1325 graphics=True):
1326 '''this adds normal and special text. call this to add text'''
1327 if xhtml:
1328 try:
1329 if name and (text.startswith('/me ') or text.startswith('/me\n')):
1330 xhtml = xhtml.replace('/me', '<i>* %s</i>' % (name,), 1)
1331 self.tv.display_html(xhtml.encode('utf-8'), self)
1332 return
1333 except Exception, e:
1334 gajim.log.debug('Error processing xhtml' + str(e))
1335 gajim.log.debug('with |' + xhtml + '|')
1337 # /me is replaced by name if name is given
1338 if name and (text.startswith('/me ') or text.startswith('/me\n')):
1339 text = '* ' + name + text[3:]
1340 text_tags.append('italic')
1341 # detect urls formatting and if the user has it on emoticons
1342 self.detect_and_print_special_text(text, text_tags, graphics=graphics)
1344 # vim: se ts=3: