more usage of NEC to handle messages
[gajim.git] / src / conversation_textview.py
blob5c45e1eccdceb5f174d9c9cf8947408fd776e505
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-2010 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
41 import urllib
43 import gtkgui_helpers
44 from common import gajim
45 from common import helpers
46 from common import latex
47 from common import i18n
48 from calendar import timegm
49 from common.fuzzyclock import FuzzyClock
51 from htmltextview import HtmlTextView
52 from common.exceptions import GajimGeneralException
53 from common.exceptions import LatexError
55 NOT_SHOWN = 0
56 ALREADY_RECEIVED = 1
57 SHOWN = 2
59 def is_selection_modified(mark):
60 name = mark.get_name()
61 if name and name in ('selection_bound', 'insert'):
62 return True
63 else:
64 return False
66 def has_focus(widget):
67 return widget.flags() & gtk.HAS_FOCUS == gtk.HAS_FOCUS
69 class TextViewImage(gtk.Image):
71 def __init__(self, anchor, text):
72 super(TextViewImage, self).__init__()
73 self.anchor = anchor
74 self._selected = False
75 self._disconnect_funcs = []
76 self.connect('parent-set', self.on_parent_set)
77 self.connect('expose-event', self.on_expose)
78 self.set_tooltip_text(text)
79 self.anchor.set_data('plaintext', text)
81 def _get_selected(self):
82 parent = self.get_parent()
83 if not parent or not self.anchor: return False
84 buffer_ = parent.get_buffer()
85 position = buffer_.get_iter_at_child_anchor(self.anchor)
86 bounds = buffer_.get_selection_bounds()
87 if bounds and position.in_range(*bounds):
88 return True
89 else:
90 return False
92 def get_state(self):
93 parent = self.get_parent()
94 if not parent:
95 return gtk.STATE_NORMAL
96 if self._selected:
97 if has_focus(parent):
98 return gtk.STATE_SELECTED
99 else:
100 return gtk.STATE_ACTIVE
101 else:
102 return gtk.STATE_NORMAL
104 def _update_selected(self):
105 selected = self._get_selected()
106 if self._selected != selected:
107 self._selected = selected
108 self.queue_draw()
110 def _do_connect(self, widget, signal, callback):
111 id_ = widget.connect(signal, callback)
112 def disconnect():
113 widget.disconnect(id_)
114 self._disconnect_funcs.append(disconnect)
116 def _disconnect_signals(self):
117 for func in self._disconnect_funcs:
118 func()
119 self._disconnect_funcs = []
121 def on_parent_set(self, widget, old_parent):
122 parent = self.get_parent()
123 if not parent:
124 self._disconnect_signals()
125 return
127 self._do_connect(parent, 'style-set', self.do_queue_draw)
128 self._do_connect(parent, 'focus-in-event', self.do_queue_draw)
129 self._do_connect(parent, 'focus-out-event', self.do_queue_draw)
131 textbuf = parent.get_buffer()
132 self._do_connect(textbuf, 'mark-set', self.on_mark_set)
133 self._do_connect(textbuf, 'mark-deleted', self.on_mark_deleted)
135 def do_queue_draw(self, *args):
136 self.queue_draw()
137 return False
139 def on_mark_set(self, buf, iterat, mark):
140 self.on_mark_modified(mark)
141 return False
143 def on_mark_deleted(self, buf, mark):
144 self.on_mark_modified(mark)
145 return False
147 def on_mark_modified(self, mark):
148 if is_selection_modified(mark):
149 self._update_selected()
151 def on_expose(self, widget, event):
152 state = self.get_state()
153 if state != gtk.STATE_NORMAL:
154 gc = widget.get_style().base_gc[state]
155 area = widget.allocation
156 widget.window.draw_rectangle(gc, True, area.x, area.y,
157 area.width, area.height)
158 return False
161 class ConversationTextview(gobject.GObject):
163 Class for the conversation textview (where user reads already said messages)
164 for chat/groupchat windows
166 __gsignals__ = dict(
167 quote = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION,
168 None, # return value
169 (str, ) # arguments
173 FOCUS_OUT_LINE_PIXBUF = gtkgui_helpers.get_icon_pixmap('gajim-muc_separator')
174 XEP0184_WARNING_PIXBUF = gtkgui_helpers.get_icon_pixmap(
175 'gajim-receipt_missing')
177 # smooth scroll constants
178 MAX_SCROLL_TIME = 0.4 # seconds
179 SCROLL_DELAY = 33 # milliseconds
181 def __init__(self, account, used_in_history_window = False):
183 If used_in_history_window is True, then we do not show Clear menuitem in
184 context menu
186 gobject.GObject.__init__(self)
187 self.used_in_history_window = used_in_history_window
189 self.fc = FuzzyClock()
192 # no need to inherit TextView, use it as atrribute is safer
193 self.tv = HtmlTextView()
194 self.tv.hyperlink_handler = self.hyperlink_handler
196 # set properties
197 self.tv.set_border_width(1)
198 self.tv.set_accepts_tab(True)
199 self.tv.set_editable(False)
200 self.tv.set_cursor_visible(False)
201 self.tv.set_wrap_mode(gtk.WRAP_WORD_CHAR)
202 self.tv.set_left_margin(2)
203 self.tv.set_right_margin(2)
204 self.handlers = {}
205 self.images = []
206 self.image_cache = {}
207 self.xep0184_marks = {}
208 self.xep0184_shown = {}
210 # It's True when we scroll in the code, so we can detect scroll from user
211 self.auto_scrolling = False
213 # connect signals
214 id_ = self.tv.connect('motion_notify_event',
215 self.on_textview_motion_notify_event)
216 self.handlers[id_] = self.tv
217 id_ = self.tv.connect('populate_popup', self.on_textview_populate_popup)
218 self.handlers[id_] = self.tv
219 id_ = self.tv.connect('button_press_event',
220 self.on_textview_button_press_event)
221 self.handlers[id_] = self.tv
223 id_ = self.tv.connect('expose-event',
224 self.on_textview_expose_event)
225 self.handlers[id_] = self.tv
228 self.account = account
229 self.change_cursor = False
230 self.last_time_printout = 0
232 font = pango.FontDescription(gajim.config.get('conversation_font'))
233 self.tv.modify_font(font)
234 buffer_ = self.tv.get_buffer()
235 end_iter = buffer_.get_end_iter()
236 buffer_.create_mark('end', end_iter, False)
238 self.tagIn = buffer_.create_tag('incoming')
239 color = gajim.config.get('inmsgcolor')
240 font = pango.FontDescription(gajim.config.get('inmsgfont'))
241 self.tagIn.set_property('foreground', color)
242 self.tagIn.set_property('font-desc', font)
244 self.tagOut = buffer_.create_tag('outgoing')
245 color = gajim.config.get('outmsgcolor')
246 font = pango.FontDescription(gajim.config.get('outmsgfont'))
247 self.tagOut.set_property('foreground', color)
248 self.tagOut.set_property('font-desc', font)
250 self.tagStatus = buffer_.create_tag('status')
251 color = gajim.config.get('statusmsgcolor')
252 font = pango.FontDescription(gajim.config.get('satusmsgfont'))
253 self.tagStatus.set_property('foreground', color)
254 self.tagStatus.set_property('font-desc', font)
256 self.tagInText = buffer_.create_tag('incomingtxt')
257 color = gajim.config.get('inmsgtxtcolor')
258 font = pango.FontDescription(gajim.config.get('inmsgtxtfont'))
259 if color:
260 self.tagInText.set_property('foreground', color)
261 self.tagInText.set_property('font-desc', font)
263 self.tagOutText = buffer_.create_tag('outgoingtxt')
264 color = gajim.config.get('outmsgtxtcolor')
265 if color:
266 font = pango.FontDescription(gajim.config.get('outmsgtxtfont'))
267 self.tagOutText.set_property('foreground', color)
268 self.tagOutText.set_property('font-desc', font)
270 colors = gajim.config.get('gc_nicknames_colors')
271 colors = colors.split(':')
272 for i, color in enumerate(colors):
273 tagname = 'gc_nickname_color_' + str(i)
274 tag = buffer_.create_tag(tagname)
275 tag.set_property('foreground', color)
277 tag = buffer_.create_tag('marked')
278 color = gajim.config.get('markedmsgcolor')
279 tag.set_property('foreground', color)
280 tag.set_property('weight', pango.WEIGHT_BOLD)
282 tag = buffer_.create_tag('time_sometimes')
283 tag.set_property('foreground', 'darkgrey')
284 tag.set_property('scale', pango.SCALE_SMALL)
285 tag.set_property('justification', gtk.JUSTIFY_CENTER)
287 tag = buffer_.create_tag('small')
288 tag.set_property('scale', pango.SCALE_SMALL)
290 tag = buffer_.create_tag('restored_message')
291 color = gajim.config.get('restored_messages_color')
292 tag.set_property('foreground', color)
294 self.tagURL = buffer_.create_tag('url')
295 color = gajim.config.get('urlmsgcolor')
296 self.tagURL.set_property('foreground', color)
297 self.tagURL.set_property('underline', pango.UNDERLINE_SINGLE)
298 id_ = self.tagURL.connect('event', self.hyperlink_handler, 'url')
299 self.handlers[id_] = self.tagURL
301 self.tagMail = buffer_.create_tag('mail')
302 self.tagMail.set_property('foreground', color)
303 self.tagMail.set_property('underline', pango.UNDERLINE_SINGLE)
304 id_ = self.tagMail.connect('event', self.hyperlink_handler, 'mail')
305 self.handlers[id_] = self.tagMail
307 self.tagXMPP = buffer_.create_tag('xmpp')
308 self.tagXMPP.set_property('foreground', color)
309 self.tagXMPP.set_property('underline', pango.UNDERLINE_SINGLE)
310 id_ = self.tagXMPP.connect('event', self.hyperlink_handler, 'xmpp')
311 self.handlers[id_] = self.tagXMPP
313 self.tagSthAtSth = buffer_.create_tag('sth_at_sth')
314 self.tagSthAtSth.set_property('foreground', color)
315 self.tagSthAtSth.set_property('underline', pango.UNDERLINE_SINGLE)
316 id_ = self.tagSthAtSth.connect('event', self.hyperlink_handler,
317 'sth_at_sth')
318 self.handlers[id_] = self.tagSthAtSth
320 tag = buffer_.create_tag('bold')
321 tag.set_property('weight', pango.WEIGHT_BOLD)
323 tag = buffer_.create_tag('italic')
324 tag.set_property('style', pango.STYLE_ITALIC)
326 tag = buffer_.create_tag('underline')
327 tag.set_property('underline', pango.UNDERLINE_SINGLE)
329 buffer_.create_tag('focus-out-line', justification = gtk.JUSTIFY_CENTER)
330 self.displaymarking_tags = {}
332 tag = buffer_.create_tag('xep0184-warning')
334 # One mark at the begining then 2 marks between each lines
335 size = gajim.config.get('max_conversation_lines')
336 size = 2 * size - 1
337 self.marks_queue = Queue.Queue(size)
339 self.allow_focus_out_line = True
340 # holds a mark at the end of --- line
341 self.focus_out_end_mark = None
343 self.xep0184_warning_tooltip = tooltips.BaseTooltip()
345 self.line_tooltip = tooltips.BaseTooltip()
346 # use it for hr too
347 self.tv.focus_out_line_pixbuf = ConversationTextview.FOCUS_OUT_LINE_PIXBUF
348 self.smooth_id = None
350 def del_handlers(self):
351 for i in self.handlers.keys():
352 if self.handlers[i].handler_is_connected(i):
353 self.handlers[i].disconnect(i)
354 del self.handlers
355 self.tv.destroy()
356 #FIXME:
357 # self.line_tooltip.destroy()
359 def update_tags(self):
360 self.tagIn.set_property('foreground', gajim.config.get('inmsgcolor'))
361 self.tagOut.set_property('foreground', gajim.config.get('outmsgcolor'))
362 self.tagStatus.set_property('foreground',
363 gajim.config.get('statusmsgcolor'))
364 self.tagURL.set_property('foreground', gajim.config.get('urlmsgcolor'))
365 self.tagMail.set_property('foreground', gajim.config.get('urlmsgcolor'))
367 def at_the_end(self):
368 buffer_ = self.tv.get_buffer()
369 end_iter = buffer_.get_end_iter()
370 end_rect = self.tv.get_iter_location(end_iter)
371 visible_rect = self.tv.get_visible_rect()
372 if end_rect.y <= (visible_rect.y + visible_rect.height):
373 return True
374 return False
376 # Smooth scrolling inspired by Pidgin code
377 def smooth_scroll(self):
378 parent = self.tv.get_parent()
379 if not parent:
380 return False
381 vadj = parent.get_vadjustment()
382 max_val = vadj.upper - vadj.page_size + 1
383 cur_val = vadj.get_value()
384 # scroll by 1/3rd of remaining distance
385 onethird = cur_val + ((max_val - cur_val) / 3.0)
386 self.auto_scrolling = True
387 vadj.set_value(onethird)
388 self.auto_scrolling = False
389 if max_val - onethird < 0.01:
390 self.smooth_id = None
391 self.smooth_scroll_timer.cancel()
392 return False
393 return True
395 def smooth_scroll_timeout(self):
396 gobject.idle_add(self.do_smooth_scroll_timeout)
397 return
399 def do_smooth_scroll_timeout(self):
400 if not self.smooth_id:
401 # we finished scrolling
402 return
403 gobject.source_remove(self.smooth_id)
404 self.smooth_id = None
405 parent = self.tv.get_parent()
406 if parent:
407 vadj = parent.get_vadjustment()
408 self.auto_scrolling = True
409 vadj.set_value(vadj.upper - vadj.page_size + 1)
410 self.auto_scrolling = False
412 def smooth_scroll_to_end(self):
413 if None != self.smooth_id: # already scrolling
414 return False
415 self.smooth_id = gobject.timeout_add(self.SCROLL_DELAY,
416 self.smooth_scroll)
417 self.smooth_scroll_timer = Timer(self.MAX_SCROLL_TIME,
418 self.smooth_scroll_timeout)
419 self.smooth_scroll_timer.start()
420 return False
422 def scroll_to_end(self):
423 parent = self.tv.get_parent()
424 buffer_ = self.tv.get_buffer()
425 end_mark = buffer_.get_mark('end')
426 if not end_mark:
427 return False
428 self.auto_scrolling = True
429 self.tv.scroll_to_mark(end_mark, 0, True, 0, 1)
430 adjustment = parent.get_hadjustment()
431 adjustment.set_value(0)
432 self.auto_scrolling = False
433 return False # when called in an idle_add, just do it once
435 def bring_scroll_to_end(self, diff_y = 0,
436 use_smooth=gajim.config.get('use_smooth_scrolling')):
437 ''' scrolls to the end of textview if end is not visible '''
438 buffer_ = self.tv.get_buffer()
439 end_iter = buffer_.get_end_iter()
440 end_rect = self.tv.get_iter_location(end_iter)
441 visible_rect = self.tv.get_visible_rect()
442 # scroll only if expected end is not visible
443 if end_rect.y >= (visible_rect.y + visible_rect.height + diff_y):
444 if use_smooth:
445 gobject.idle_add(self.smooth_scroll_to_end)
446 else:
447 gobject.idle_add(self.scroll_to_end_iter)
449 def scroll_to_end_iter(self):
450 buffer_ = self.tv.get_buffer()
451 end_iter = buffer_.get_end_iter()
452 if not end_iter:
453 return False
454 self.tv.scroll_to_iter(end_iter, 0, False, 1, 1)
455 return False # when called in an idle_add, just do it once
457 def stop_scrolling(self):
458 if self.smooth_id:
459 gobject.source_remove(self.smooth_id)
460 self.smooth_id = None
461 self.smooth_scroll_timer.cancel()
463 def show_xep0184_warning(self, id_):
464 if id_ in self.xep0184_marks:
465 return
467 buffer_ = self.tv.get_buffer()
468 buffer_.begin_user_action()
470 self.xep0184_marks[id_] = buffer_.create_mark(None,
471 buffer_.get_end_iter(), left_gravity=True)
472 self.xep0184_shown[id_] = NOT_SHOWN
474 def show_it():
475 if (not id_ in self.xep0184_shown) or \
476 self.xep0184_shown[id_] == ALREADY_RECEIVED:
477 return False
479 end_iter = buffer_.get_iter_at_mark(self.xep0184_marks[id_])
480 buffer_.insert(end_iter, ' ')
481 anchor = buffer_.create_child_anchor(end_iter)
482 img = TextViewImage(anchor, '')
483 img.set_from_pixbuf(ConversationTextview.XEP0184_WARNING_PIXBUF)
484 img.show()
485 self.tv.add_child_at_anchor(img, anchor)
486 before_img_iter = buffer_.get_iter_at_mark(self.xep0184_marks[id_])
487 before_img_iter.forward_char()
488 post_img_iter = before_img_iter.copy()
489 post_img_iter.forward_char()
490 buffer_.apply_tag_by_name('xep0184-warning', before_img_iter,
491 post_img_iter)
493 self.xep0184_shown[id_] = SHOWN
494 return False
495 gobject.timeout_add_seconds(3, show_it)
497 buffer_.end_user_action()
499 def hide_xep0184_warning(self, id_):
500 if id_ not in self.xep0184_marks:
501 return
503 if self.xep0184_shown[id_] == NOT_SHOWN:
504 self.xep0184_shown[id_] = ALREADY_RECEIVED
505 return
507 buffer_ = self.tv.get_buffer()
508 buffer_.begin_user_action()
510 begin_iter = buffer_.get_iter_at_mark(self.xep0184_marks[id_])
512 end_iter = begin_iter.copy()
513 # XXX: Is there a nicer way?
514 end_iter.forward_char()
515 end_iter.forward_char()
517 buffer_.delete(begin_iter, end_iter)
518 buffer_.delete_mark(self.xep0184_marks[id_])
520 buffer_.end_user_action()
522 del self.xep0184_marks[id_]
523 del self.xep0184_shown[id_]
525 def show_focus_out_line(self):
526 if not self.allow_focus_out_line:
527 # if room did not receive focus-in from the last time we added
528 # --- line then do not readd
529 return
531 print_focus_out_line = False
532 buffer_ = self.tv.get_buffer()
534 if self.focus_out_end_mark is None:
535 # this happens only first time we focus out on this room
536 print_focus_out_line = True
538 else:
539 focus_out_end_iter = buffer_.get_iter_at_mark(self.focus_out_end_mark)
540 focus_out_end_iter_offset = focus_out_end_iter.get_offset()
541 if focus_out_end_iter_offset != buffer_.get_end_iter().get_offset():
542 # this means after last-focus something was printed
543 # (else end_iter's offset is the same as before)
544 # only then print ---- line (eg. we avoid printing many following
545 # ---- lines)
546 print_focus_out_line = True
548 if print_focus_out_line and buffer_.get_char_count() > 0:
549 buffer_.begin_user_action()
551 # remove previous focus out line if such focus out line exists
552 if self.focus_out_end_mark is not None:
553 end_iter_for_previous_line = buffer_.get_iter_at_mark(
554 self.focus_out_end_mark)
555 begin_iter_for_previous_line = end_iter_for_previous_line.copy()
556 # img_char+1 (the '\n')
557 begin_iter_for_previous_line.backward_chars(2)
559 # remove focus out line
560 buffer_.delete(begin_iter_for_previous_line,
561 end_iter_for_previous_line)
562 buffer_.delete_mark(self.focus_out_end_mark)
564 # add the new focus out line
565 end_iter = buffer_.get_end_iter()
566 buffer_.insert(end_iter, '\n')
567 buffer_.insert_pixbuf(end_iter,
568 ConversationTextview.FOCUS_OUT_LINE_PIXBUF)
570 end_iter = buffer_.get_end_iter()
571 before_img_iter = end_iter.copy()
572 # one char back (an image also takes one char)
573 before_img_iter.backward_char()
574 buffer_.apply_tag_by_name('focus-out-line', before_img_iter, end_iter)
576 self.allow_focus_out_line = False
578 # update the iter we hold to make comparison the next time
579 self.focus_out_end_mark = buffer_.create_mark(None,
580 buffer_.get_end_iter(), left_gravity=True)
582 buffer_.end_user_action()
584 # scroll to the end (via idle in case the scrollbar has appeared)
585 gobject.idle_add(self.scroll_to_end)
587 def show_xep0184_warning_tooltip(self):
588 pointer = self.tv.get_pointer()
589 x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
590 pointer[0], pointer[1])
591 tags = self.tv.get_iter_at_location(x, y).get_tags()
592 tag_table = self.tv.get_buffer().get_tag_table()
593 xep0184_warning = False
594 for tag in tags:
595 if tag == tag_table.lookup('xep0184-warning'):
596 xep0184_warning = True
597 break
598 if xep0184_warning and not self.xep0184_warning_tooltip.win:
599 # check if the current pointer is still over the line
600 position = self.tv.window.get_origin()
601 self.xep0184_warning_tooltip.show_tooltip(_('This icon indicates that '
602 'this message has not yet\nbeen received by the remote end. '
603 "If this icon stays\nfor a long time, it's likely the message got "
604 'lost.'), 8, position[1] + pointer[1])
606 def show_line_tooltip(self):
607 pointer = self.tv.get_pointer()
608 x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
609 pointer[0], pointer[1])
610 tags = self.tv.get_iter_at_location(x, y).get_tags()
611 tag_table = self.tv.get_buffer().get_tag_table()
612 over_line = False
613 for tag in tags:
614 if tag == tag_table.lookup('focus-out-line'):
615 over_line = True
616 break
617 if over_line and not self.line_tooltip.win:
618 # check if the current pointer is still over the line
619 position = self.tv.window.get_origin()
620 self.line_tooltip.show_tooltip(_('Text below this line is what has '
621 'been said since the\nlast time you paid attention to this group '
622 'chat'), 8, position[1] + pointer[1])
624 def on_textview_expose_event(self, widget, event):
625 expalloc = event.area
626 exp_x0 = expalloc.x
627 exp_y0 = expalloc.y
628 exp_x1 = exp_x0 + expalloc.width
629 exp_y1 = exp_y0 + expalloc.height
631 try:
632 tryfirst = [self.image_cache[(exp_x0, exp_y0)]]
633 except KeyError:
634 tryfirst = []
636 for image in tryfirst + self.images:
637 imgalloc = image.allocation
638 img_x0 = imgalloc.x
639 img_y0 = imgalloc.y
640 img_x1 = img_x0 + imgalloc.width
641 img_y1 = img_y0 + imgalloc.height
643 if img_x0 <= exp_x0 and img_y0 <= exp_y0 and \
644 exp_x1 <= img_x1 and exp_y1 <= img_y1:
645 self.image_cache[(img_x0, img_y0)] = image
646 widget.propagate_expose(image, event)
647 return True
648 return False
650 def on_textview_motion_notify_event(self, widget, event):
652 Change the cursor to a hand when we are over a mail or an url
654 pointer_x, pointer_y = self.tv.window.get_pointer()[0:2]
655 x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
656 pointer_x, pointer_y)
657 tags = self.tv.get_iter_at_location(x, y).get_tags()
658 if self.change_cursor:
659 self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
660 gtk.gdk.Cursor(gtk.gdk.XTERM))
661 self.change_cursor = False
662 tag_table = self.tv.get_buffer().get_tag_table()
663 over_line = False
664 xep0184_warning = False
665 for tag in tags:
666 if tag in (tag_table.lookup('url'), tag_table.lookup('mail'), \
667 tag_table.lookup('xmpp'), tag_table.lookup('sth_at_sth')):
668 self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
669 gtk.gdk.Cursor(gtk.gdk.HAND2))
670 self.change_cursor = True
671 elif tag == tag_table.lookup('focus-out-line'):
672 over_line = True
673 elif tag == tag_table.lookup('xep0184-warning'):
674 xep0184_warning = True
676 if self.line_tooltip.timeout != 0:
677 # Check if we should hide the line tooltip
678 if not over_line:
679 self.line_tooltip.hide_tooltip()
680 if self.xep0184_warning_tooltip.timeout != 0:
681 # Check if we should hide the XEP-184 warning tooltip
682 if not xep0184_warning:
683 self.xep0184_warning_tooltip.hide_tooltip()
684 if over_line and not self.line_tooltip.win:
685 self.line_tooltip.timeout = gobject.timeout_add(500,
686 self.show_line_tooltip)
687 self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
688 gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
689 self.change_cursor = True
690 if xep0184_warning and not self.xep0184_warning_tooltip.win:
691 self.xep0184_warning_tooltip.timeout = gobject.timeout_add(500,
692 self.show_xep0184_warning_tooltip)
693 self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
694 gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
695 self.change_cursor = True
697 def clear(self, tv = None):
699 Clear text in the textview
701 buffer_ = self.tv.get_buffer()
702 start, end = buffer_.get_bounds()
703 buffer_.delete(start, end)
704 size = gajim.config.get('max_conversation_lines')
705 size = 2 * size - 1
706 self.marks_queue = Queue.Queue(size)
707 self.focus_out_end_mark = None
708 self.just_cleared = True
710 def visit_url_from_menuitem(self, widget, link):
712 Basically it filters out the widget instance
714 helpers.launch_browser_mailer('url', link)
716 def on_textview_populate_popup(self, textview, menu):
718 Override the default context menu and we prepend Clear (only if
719 used_in_history_window is False) and if we have sth selected we show a
720 submenu with actions on the phrase (see
721 on_conversation_textview_button_press_event)
723 separator_menuitem_was_added = False
724 if not self.used_in_history_window:
725 item = gtk.SeparatorMenuItem()
726 menu.prepend(item)
727 separator_menuitem_was_added = True
729 item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
730 menu.prepend(item)
731 id_ = item.connect('activate', self.clear)
732 self.handlers[id_] = item
734 if self.selected_phrase:
735 if not separator_menuitem_was_added:
736 item = gtk.SeparatorMenuItem()
737 menu.prepend(item)
739 if not self.used_in_history_window:
740 item = gtk.MenuItem(_('_Quote'))
741 id_ = item.connect('activate', self.on_quote)
742 self.handlers[id_] = item
743 menu.prepend(item)
745 _selected_phrase = helpers.reduce_chars_newlines(
746 self.selected_phrase, 25, 2)
747 item = gtk.MenuItem(_('_Actions for "%s"') % _selected_phrase)
748 menu.prepend(item)
749 submenu = gtk.Menu()
750 item.set_submenu(submenu)
751 phrase_for_url = urllib.quote(self.selected_phrase.encode('utf-8'))
753 always_use_en = gajim.config.get('always_english_wikipedia')
754 if always_use_en:
755 link = 'http://en.wikipedia.org/wiki/Special:Search?search=%s'\
756 % phrase_for_url
757 else:
758 link = 'http://%s.wikipedia.org/wiki/Special:Search?search=%s'\
759 % (gajim.LANG, phrase_for_url)
760 item = gtk.MenuItem(_('Read _Wikipedia Article'))
761 id_ = item.connect('activate', self.visit_url_from_menuitem, link)
762 self.handlers[id_] = item
763 submenu.append(item)
765 item = gtk.MenuItem(_('Look it up in _Dictionary'))
766 dict_link = gajim.config.get('dictionary_url')
767 if dict_link == 'WIKTIONARY':
768 # special link (yeah undocumented but default)
769 always_use_en = gajim.config.get('always_english_wiktionary')
770 if always_use_en:
771 link = 'http://en.wiktionary.org/wiki/Special:Search?search=%s'\
772 % phrase_for_url
773 else:
774 link = 'http://%s.wiktionary.org/wiki/Special:Search?search=%s'\
775 % (gajim.LANG, phrase_for_url)
776 id_ = item.connect('activate', self.visit_url_from_menuitem, link)
777 self.handlers[id_] = item
778 else:
779 if dict_link.find('%s') == -1:
780 # we must have %s in the url if not WIKTIONARY
781 item = gtk.MenuItem(_(
782 'Dictionary URL is missing an "%s" and it is not WIKTIONARY'))
783 item.set_property('sensitive', False)
784 else:
785 link = dict_link % phrase_for_url
786 id_ = item.connect('activate', self.visit_url_from_menuitem,
787 link)
788 self.handlers[id_] = item
789 submenu.append(item)
792 search_link = gajim.config.get('search_engine')
793 if search_link.find('%s') == -1:
794 # we must have %s in the url
795 item = gtk.MenuItem(_('Web Search URL is missing an "%s"'))
796 item.set_property('sensitive', False)
797 else:
798 item = gtk.MenuItem(_('Web _Search for it'))
799 link = search_link % phrase_for_url
800 id_ = item.connect('activate', self.visit_url_from_menuitem, link)
801 self.handlers[id_] = item
802 submenu.append(item)
804 item = gtk.MenuItem(_('Open as _Link'))
805 id_ = item.connect('activate', self.visit_url_from_menuitem, link)
806 self.handlers[id_] = item
807 submenu.append(item)
809 menu.show_all()
811 def on_quote(self, widget):
812 self.emit('quote', self.selected_phrase)
814 def on_textview_button_press_event(self, widget, event):
815 # If we clicked on a taged text do NOT open the standard popup menu
816 # if normal text check if we have sth selected
817 self.selected_phrase = '' # do not move belove event button check!
819 if event.button != 3: # if not right click
820 return False
822 x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
823 int(event.x), int(event.y))
824 iter_ = self.tv.get_iter_at_location(x, y)
825 tags = iter_.get_tags()
828 if tags: # we clicked on sth special (it can be status message too)
829 for tag in tags:
830 tag_name = tag.get_property('name')
831 if tag_name in ('url', 'mail', 'xmpp', 'sth_at_sth'):
832 return True # we block normal context menu
834 # we check if sth was selected and if it was we assign
835 # selected_phrase variable
836 # so on_conversation_textview_populate_popup can use it
837 buffer_ = self.tv.get_buffer()
838 return_val = buffer_.get_selection_bounds()
839 if return_val: # if sth was selected when we right-clicked
840 # get the selected text
841 start_sel, finish_sel = return_val[0], return_val[1]
842 self.selected_phrase = buffer_.get_text(start_sel, finish_sel).decode(
843 'utf-8')
844 elif ord(iter_.get_char()) > 31:
845 # we clicked on a word, do as if it's selected for context menu
846 start_sel = iter_.copy()
847 if not start_sel.starts_word():
848 start_sel.backward_word_start()
849 finish_sel = iter_.copy()
850 if not finish_sel.ends_word():
851 finish_sel.forward_word_end()
852 self.selected_phrase = buffer_.get_text(start_sel, finish_sel).decode(
853 'utf-8')
855 def on_open_link_activate(self, widget, kind, text):
856 helpers.launch_browser_mailer(kind, text)
858 def on_copy_link_activate(self, widget, text):
859 clip = gtk.clipboard_get()
860 clip.set_text(text)
862 def on_start_chat_activate(self, widget, jid):
863 gajim.interface.new_chat_from_jid(self.account, jid)
865 def on_join_group_chat_menuitem_activate(self, widget, room_jid):
866 if 'join_gc' in gajim.interface.instances[self.account]:
867 instance = gajim.interface.instances[self.account]['join_gc']
868 instance.xml.get_object('room_jid_entry').set_text(room_jid)
869 gajim.interface.instances[self.account]['join_gc'].window.present()
870 else:
871 try:
872 dialogs.JoinGroupchatWindow(account=self.account, room_jid=room_jid)
873 except GajimGeneralException:
874 pass
876 def on_add_to_roster_activate(self, widget, jid):
877 dialogs.AddNewContactWindow(self.account, jid)
879 def make_link_menu(self, event, kind, text):
880 xml = gtkgui_helpers.get_gtk_builder('chat_context_menu.ui')
881 menu = xml.get_object('chat_context_menu')
882 childs = menu.get_children()
883 if kind == 'url':
884 id_ = childs[0].connect('activate', self.on_copy_link_activate, text)
885 self.handlers[id_] = childs[0]
886 id_ = childs[1].connect('activate', self.on_open_link_activate, kind,
887 text)
888 self.handlers[id_] = childs[1]
889 childs[2].hide() # copy mail address
890 childs[3].hide() # open mail composer
891 childs[4].hide() # jid section separator
892 childs[5].hide() # start chat
893 childs[6].hide() # join group chat
894 childs[7].hide() # add to roster
895 else: # It's a mail or a JID
896 # load muc icon
897 join_group_chat_menuitem = xml.get_object('join_group_chat_menuitem')
898 muc_icon = gtkgui_helpers.load_icon('muc_active')
899 if muc_icon:
900 join_group_chat_menuitem.set_image(muc_icon)
902 text = text.lower()
903 if text.startswith('xmpp:'):
904 text = text[5:]
905 id_ = childs[2].connect('activate', self.on_copy_link_activate, text)
906 self.handlers[id_] = childs[2]
907 id_ = childs[3].connect('activate', self.on_open_link_activate, kind,
908 text)
909 self.handlers[id_] = childs[3]
910 id_ = childs[5].connect('activate', self.on_start_chat_activate, text)
911 self.handlers[id_] = childs[5]
912 id_ = childs[6].connect('activate',
913 self.on_join_group_chat_menuitem_activate, text)
914 self.handlers[id_] = childs[6]
916 if self.account:
917 id_ = childs[7].connect('activate', self.on_add_to_roster_activate,
918 text)
919 self.handlers[id_] = childs[7]
920 childs[7].show() # show add to roster menuitem
921 else:
922 childs[7].hide() # hide add to roster menuitem
924 if kind == 'xmpp':
925 childs[2].hide() # copy mail address
926 childs[3].hide() # open mail composer
927 childs[4].hide() # jid section separator
928 elif kind == 'mail':
929 childs[4].hide() # jid section separator
930 childs[5].hide() # start chat
931 childs[6].hide() # join group chat
932 childs[7].hide() # add to roster
934 childs[0].hide() # copy link location
935 childs[1].hide() # open link in browser
937 menu.popup(None, None, None, event.button, event.time)
939 def hyperlink_handler(self, texttag, widget, event, iter_, kind):
940 if event.type == gtk.gdk.BUTTON_PRESS:
941 begin_iter = iter_.copy()
942 # we get the begining of the tag
943 while not begin_iter.begins_tag(texttag):
944 begin_iter.backward_char()
945 end_iter = iter_.copy()
946 # we get the end of the tag
947 while not end_iter.ends_tag(texttag):
948 end_iter.forward_char()
950 # Detect XHTML-IM link
951 word = getattr(texttag, 'href', None)
952 if word:
953 if word.startswith('xmpp'):
954 kind = 'xmpp'
955 elif word.startswith('mailto:'):
956 kind = 'mail'
957 elif gajim.interface.sth_at_sth_dot_sth_re.match(word):
958 # it's a JID or mail
959 kind = 'sth_at_sth'
960 else:
961 word = self.tv.get_buffer().get_text(begin_iter, end_iter).decode(
962 'utf-8')
964 if event.button == 3: # right click
965 self.make_link_menu(event, kind, word)
966 return True
967 else:
968 # we launch the correct application
969 if kind == 'xmpp':
970 word = word[5:]
971 if '?' in word:
972 (jid, action) = word.split('?')
973 if action == 'join':
974 self.on_join_group_chat_menuitem_activate(None, jid)
975 else:
976 self.on_start_chat_activate(None, jid)
977 else:
978 self.on_start_chat_activate(None, word)
979 else:
980 helpers.launch_browser_mailer(kind, word)
982 def detect_and_print_special_text(self, otext, other_tags, graphics=True):
984 Detect special text (emots & links & formatting), print normal text
985 before any special text it founds, then print special text (that happens
986 many times until last special text is printed) and then return the index
987 after *last* special text, so we can print it in
988 print_conversation_line()
990 buffer_ = self.tv.get_buffer()
992 insert_tags_func = buffer_.insert_with_tags_by_name
993 # detect_and_print_special_text() is also used by
994 # HtmlHandler.handle_specials() and there tags is gtk.TextTag objects,
995 # not strings
996 if other_tags and isinstance(other_tags[0], gtk.TextTag):
997 insert_tags_func = buffer_.insert_with_tags
999 index = 0
1001 # Too many special elements (emoticons, LaTeX formulas, etc)
1002 # may cause Gajim to freeze (see #5129).
1003 # We impose an arbitrary limit of 100 specials per message.
1004 specials_limit = 100
1006 # basic: links + mail + formatting is always checked (we like that)
1007 if gajim.config.get('emoticons_theme') and graphics:
1008 # search for emoticons & urls
1009 iterator = gajim.interface.emot_and_basic_re.finditer(otext)
1010 else: # search for just urls + mail + formatting
1011 iterator = gajim.interface.basic_pattern_re.finditer(otext)
1012 for match in iterator:
1013 start, end = match.span()
1014 special_text = otext[start:end]
1015 if start > index:
1016 text_before_special_text = otext[index:start]
1017 end_iter = buffer_.get_end_iter()
1018 # we insert normal text
1019 insert_tags_func(end_iter, text_before_special_text, *other_tags)
1020 index = end # update index
1022 # now print it
1023 self.print_special_text(special_text, other_tags, graphics=graphics)
1024 specials_limit -= 1
1025 if specials_limit <= 0:
1026 break
1028 # add the rest of text located in the index and after
1029 end_iter = buffer_.get_end_iter()
1030 insert_tags_func(end_iter, otext[index:], *other_tags)
1032 return buffer_.get_end_iter()
1034 def print_special_text(self, special_text, other_tags, graphics=True):
1036 Is called by detect_and_print_special_text and prints special text
1037 (emots, links, formatting)
1039 tags = []
1040 use_other_tags = True
1041 text_is_valid_uri = False
1042 is_xhtml_link = None
1043 show_ascii_formatting_chars = \
1044 gajim.config.get('show_ascii_formatting_chars')
1045 buffer_ = self.tv.get_buffer()
1047 # Detect XHTML-IM link
1048 ttt = buffer_.get_tag_table()
1049 tags_ = [(ttt.lookup(t) if isinstance(t, str) else t) for t in other_tags]
1050 for t in tags_:
1051 is_xhtml_link = getattr(t, 'href', None)
1052 if is_xhtml_link:
1053 break
1055 # Check if we accept this as an uri
1056 schemes = gajim.config.get('uri_schemes').split()
1057 for scheme in schemes:
1058 if special_text.startswith(scheme):
1059 text_is_valid_uri = True
1061 possible_emot_ascii_caps = special_text.upper() # emoticons keys are CAPS
1062 if gajim.config.get('emoticons_theme') and \
1063 possible_emot_ascii_caps in gajim.interface.emoticons.keys() and graphics:
1064 # it's an emoticon
1065 emot_ascii = possible_emot_ascii_caps
1066 end_iter = buffer_.get_end_iter()
1067 anchor = buffer_.create_child_anchor(end_iter)
1068 img = TextViewImage(anchor, special_text)
1069 animations = gajim.interface.emoticons_animations
1070 if not emot_ascii in animations:
1071 animations[emot_ascii] = gtk.gdk.PixbufAnimation(
1072 gajim.interface.emoticons[emot_ascii])
1073 img.set_from_animation(animations[emot_ascii])
1074 img.show()
1075 self.images.append(img)
1076 # add with possible animation
1077 self.tv.add_child_at_anchor(img, anchor)
1078 elif special_text.startswith('www.') or \
1079 special_text.startswith('ftp.') or \
1080 text_is_valid_uri and not is_xhtml_link:
1081 tags.append('url')
1082 elif special_text.startswith('mailto:') and not is_xhtml_link:
1083 tags.append('mail')
1084 elif special_text.startswith('xmpp:') and not is_xhtml_link:
1085 tags.append('xmpp')
1086 elif gajim.interface.sth_at_sth_dot_sth_re.match(special_text) and\
1087 not is_xhtml_link:
1088 # it's a JID or mail
1089 tags.append('sth_at_sth')
1090 elif special_text.startswith('*'): # it's a bold text
1091 tags.append('bold')
1092 if special_text[1] == '/' and special_text[-2] == '/' and\
1093 len(special_text) > 4: # it's also italic
1094 tags.append('italic')
1095 if not show_ascii_formatting_chars:
1096 special_text = special_text[2:-2] # remove */ /*
1097 elif special_text[1] == '_' and special_text[-2] == '_' and \
1098 len(special_text) > 4: # it's also underlined
1099 tags.append('underline')
1100 if not show_ascii_formatting_chars:
1101 special_text = special_text[2:-2] # remove *_ _*
1102 else:
1103 if not show_ascii_formatting_chars:
1104 special_text = special_text[1:-1] # remove * *
1105 elif special_text.startswith('/'): # it's an italic text
1106 tags.append('italic')
1107 if special_text[1] == '*' and special_text[-2] == '*' and \
1108 len(special_text) > 4: # it's also bold
1109 tags.append('bold')
1110 if not show_ascii_formatting_chars:
1111 special_text = special_text[2:-2] # remove /* */
1112 elif special_text[1] == '_' and special_text[-2] == '_' and \
1113 len(special_text) > 4: # it's also underlined
1114 tags.append('underline')
1115 if not show_ascii_formatting_chars:
1116 special_text = special_text[2:-2] # remove /_ _/
1117 else:
1118 if not show_ascii_formatting_chars:
1119 special_text = special_text[1:-1] # remove / /
1120 elif special_text.startswith('_'): # it's an underlined text
1121 tags.append('underline')
1122 if special_text[1] == '*' and special_text[-2] == '*' and \
1123 len(special_text) > 4: # it's also bold
1124 tags.append('bold')
1125 if not show_ascii_formatting_chars:
1126 special_text = special_text[2:-2] # remove _* *_
1127 elif special_text[1] == '/' and special_text[-2] == '/' and \
1128 len(special_text) > 4: # it's also italic
1129 tags.append('italic')
1130 if not show_ascii_formatting_chars:
1131 special_text = special_text[2:-2] # remove _/ /_
1132 else:
1133 if not show_ascii_formatting_chars:
1134 special_text = special_text[1:-1] # remove _ _
1135 elif gajim.HAVE_LATEX and special_text.startswith('$$') and \
1136 special_text.endswith('$$') and graphics:
1137 try:
1138 imagepath = latex.latex_to_image(special_text[2:-2])
1139 except LatexError, e:
1140 # print the error after the line has been written
1141 gobject.idle_add(self.print_conversation_line, str(e), '', 'info',
1142 '', None)
1143 imagepath = None
1144 end_iter = buffer_.get_end_iter()
1145 if imagepath is not None:
1146 anchor = buffer_.create_child_anchor(end_iter)
1147 img = TextViewImage(anchor, special_text)
1148 img.set_from_file(imagepath)
1149 img.show()
1150 # add
1151 self.tv.add_child_at_anchor(img, anchor)
1152 # delete old file
1153 try:
1154 os.remove(imagepath)
1155 except Exception:
1156 pass
1157 else:
1158 buffer_.insert(end_iter, special_text)
1159 use_other_tags = False
1160 else:
1161 # It's nothing special
1162 if use_other_tags:
1163 end_iter = buffer_.get_end_iter()
1164 insert_tags_func = buffer_.insert_with_tags_by_name
1165 if other_tags and isinstance(other_tags[0], gtk.TextTag):
1166 insert_tags_func = buffer_.insert_with_tags
1168 insert_tags_func(end_iter, special_text, *other_tags)
1170 if tags:
1171 end_iter = buffer_.get_end_iter()
1172 all_tags = tags[:]
1173 if use_other_tags:
1174 all_tags += other_tags
1175 # convert all names to TextTag
1176 all_tags = [(ttt.lookup(t) if isinstance(t, str) else t) for t in all_tags]
1177 buffer_.insert_with_tags(end_iter, special_text, *all_tags)
1179 def print_empty_line(self):
1180 buffer_ = self.tv.get_buffer()
1181 end_iter = buffer_.get_end_iter()
1182 buffer_.insert_with_tags_by_name(end_iter, '\n', 'eol')
1183 self.just_cleared = False
1185 def print_conversation_line(self, text, jid, kind, name, tim,
1186 other_tags_for_name=[], other_tags_for_time=[],
1187 other_tags_for_text=[], subject=None, old_kind=None, xhtml=None,
1188 simple=False, graphics=True, displaymarking=None):
1190 Print 'chat' type messages
1192 buffer_ = self.tv.get_buffer()
1193 buffer_.begin_user_action()
1194 if self.marks_queue.full():
1195 # remove oldest line
1196 m1 = self.marks_queue.get()
1197 m2 = self.marks_queue.get()
1198 i1 = buffer_.get_iter_at_mark(m1)
1199 i2 = buffer_.get_iter_at_mark(m2)
1200 buffer_.delete(i1, i2)
1201 buffer_.delete_mark(m1)
1202 end_iter = buffer_.get_end_iter()
1203 end_offset = end_iter.get_offset()
1204 at_the_end = self.at_the_end()
1205 move_selection = False
1206 if buffer_.get_has_selection() and buffer_.get_selection_bounds()[1].\
1207 get_offset() == end_offset:
1208 move_selection = True
1210 # Create one mark and add it to queue once if it's the first line
1211 # else twice (one for end bound, one for start bound)
1212 mark = None
1213 if buffer_.get_char_count() > 0:
1214 if not simple:
1215 buffer_.insert_with_tags_by_name(end_iter, '\n', 'eol')
1216 if move_selection:
1217 sel_start, sel_end = buffer_.get_selection_bounds()
1218 sel_end.backward_char()
1219 buffer_.select_range(sel_start, sel_end)
1220 mark = buffer_.create_mark(None, end_iter, left_gravity=True)
1221 self.marks_queue.put(mark)
1222 if not mark:
1223 mark = buffer_.create_mark(None, end_iter, left_gravity=True)
1224 self.marks_queue.put(mark)
1225 if kind == 'incoming_queue':
1226 kind = 'incoming'
1227 if old_kind == 'incoming_queue':
1228 old_kind = 'incoming'
1229 # print the time stamp
1230 if not tim:
1231 # We don't have tim for outgoing messages...
1232 tim = time.localtime()
1233 current_print_time = gajim.config.get('print_time')
1234 if current_print_time == 'always' and kind != 'info' and not simple:
1235 timestamp_str = self.get_time_to_show(tim)
1236 timestamp = time.strftime(timestamp_str, tim)
1237 buffer_.insert_with_tags_by_name(end_iter, timestamp,
1238 *other_tags_for_time)
1239 elif current_print_time == 'sometimes' and kind != 'info' and not simple:
1240 every_foo_seconds = 60 * gajim.config.get(
1241 'print_ichat_every_foo_minutes')
1242 seconds_passed = time.mktime(tim) - self.last_time_printout
1243 if seconds_passed > every_foo_seconds:
1244 self.last_time_printout = time.mktime(tim)
1245 end_iter = buffer_.get_end_iter()
1246 if gajim.config.get('print_time_fuzzy') > 0:
1247 ft = self.fc.fuzzy_time(gajim.config.get('print_time_fuzzy'), tim)
1248 tim_format = ft.decode(locale.getpreferredencoding())
1249 else:
1250 tim_format = self.get_time_to_show(tim)
1251 buffer_.insert_with_tags_by_name(end_iter, tim_format + '\n',
1252 'time_sometimes')
1253 # If there's a displaymarking, print it here.
1254 if displaymarking:
1255 self.print_displaymarking(displaymarking)
1256 # kind = info, we print things as if it was a status: same color, ...
1257 if kind in ('error', 'info'):
1258 kind = 'status'
1259 other_text_tag = self.detect_other_text_tag(text, kind)
1260 text_tags = other_tags_for_text[:] # create a new list
1261 if other_text_tag:
1262 # note that color of /me may be overwritten in gc_control
1263 text_tags.append(other_text_tag)
1264 else: # not status nor /me
1265 if gajim.config.get('chat_merge_consecutive_nickname'):
1266 if kind != old_kind or self.just_cleared:
1267 self.print_name(name, kind, other_tags_for_name)
1268 else:
1269 self.print_real_text(gajim.config.get(
1270 'chat_merge_consecutive_nickname_indent'))
1271 else:
1272 self.print_name(name, kind, other_tags_for_name)
1273 if kind == 'incoming':
1274 text_tags.append('incomingtxt')
1275 elif kind == 'outgoing':
1276 text_tags.append('outgoingtxt')
1277 self.print_subject(subject)
1278 self.print_real_text(text, text_tags, name, xhtml, graphics=graphics)
1280 # scroll to the end of the textview
1281 if at_the_end or kind == 'outgoing':
1282 # we are at the end or we are sending something
1283 # scroll to the end (via idle in case the scrollbar has appeared)
1284 if gajim.config.get('use_smooth_scrolling'):
1285 gobject.idle_add(self.smooth_scroll_to_end)
1286 else:
1287 gobject.idle_add(self.scroll_to_end)
1289 self.just_cleared = False
1290 buffer_.end_user_action()
1292 def get_time_to_show(self, tim):
1294 Get the time, with the day before if needed and return it. It DOESN'T
1295 format a fuzzy time
1297 format = ''
1298 # get difference in days since epoch (86400 = 24*3600)
1299 # number of days since epoch for current time (in GMT) -
1300 # number of days since epoch for message (in GMT)
1301 diff_day = int(timegm(time.localtime())) / 86400 -\
1302 int(timegm(tim)) / 86400
1303 if diff_day == 0:
1304 day_str = ''
1305 else:
1306 #%i is day in year (1-365)
1307 day_str = i18n.ngettext('Yesterday', '%i days ago', diff_day,
1308 replace_plural=diff_day)
1309 if day_str:
1310 format += day_str + ' '
1311 timestamp_str = gajim.config.get('time_stamp')
1312 timestamp_str = helpers.from_one_line(timestamp_str)
1313 format += timestamp_str
1314 tim_format = time.strftime(format, tim)
1315 if locale.getpreferredencoding() != 'KOI8-R':
1316 # if tim_format comes as unicode because of day_str.
1317 # we convert it to the encoding that we want (and that is utf-8)
1318 tim_format = helpers.ensure_utf8_string(tim_format)
1319 return tim_format
1321 def detect_other_text_tag(self, text, kind):
1322 if kind == 'status':
1323 return kind
1324 elif text.startswith('/me ') or text.startswith('/me\n'):
1325 return kind
1327 def print_displaymarking(self, displaymarking):
1328 bgcolor = displaymarking.getAttr('bgcolor') or '#FFF'
1329 fgcolor = displaymarking.getAttr('fgcolor') or '#000'
1330 text = displaymarking.getData()
1331 if text:
1332 buffer_ = self.tv.get_buffer()
1333 end_iter = buffer_.get_end_iter()
1334 tag = self.displaymarking_tags.setdefault(bgcolor + '/' + fgcolor,
1335 buffer_.create_tag(None, background=bgcolor, foreground=fgcolor))
1336 buffer_.insert_with_tags(end_iter, '[' + text + ']', tag)
1337 end_iter = buffer_.get_end_iter()
1338 buffer_.insert_with_tags(end_iter, ' ')
1340 def print_name(self, name, kind, other_tags_for_name):
1341 if name:
1342 buffer_ = self.tv.get_buffer()
1343 end_iter = buffer_.get_end_iter()
1344 name_tags = other_tags_for_name[:] # create a new list
1345 name_tags.append(kind)
1346 before_str = gajim.config.get('before_nickname')
1347 before_str = helpers.from_one_line(before_str)
1348 after_str = gajim.config.get('after_nickname')
1349 after_str = helpers.from_one_line(after_str)
1350 format = before_str + name + after_str + ' '
1351 buffer_.insert_with_tags_by_name(end_iter, format, *name_tags)
1353 def print_subject(self, subject):
1354 if subject: # if we have subject, show it too!
1355 subject = _('Subject: %s\n') % subject
1356 buffer_ = self.tv.get_buffer()
1357 end_iter = buffer_.get_end_iter()
1358 buffer_.insert(end_iter, subject)
1359 self.print_empty_line()
1361 def print_real_text(self, text, text_tags=[], name=None, xhtml=None,
1362 graphics=True):
1364 Add normal and special text. call this to add text
1366 if xhtml:
1367 try:
1368 if name and (text.startswith('/me ') or text.startswith('/me\n')):
1369 xhtml = xhtml.replace('/me', '<i>* %s</i>' % (name,), 1)
1370 self.tv.display_html(xhtml.encode('utf-8'), self)
1371 return
1372 except Exception, e:
1373 gajim.log.debug('Error processing xhtml' + str(e))
1374 gajim.log.debug('with |' + xhtml + '|')
1376 # /me is replaced by name if name is given
1377 if name and (text.startswith('/me ') or text.startswith('/me\n')):
1378 text = '* ' + name + text[3:]
1379 text_tags.append('italic')
1380 # detect urls formatting and if the user has it on emoticons
1381 self.detect_and_print_special_text(text, text_tags, graphics=graphics)