correctly set transient window for muc error dialogs. Fixes #6943
[gajim.git] / src / conversation_textview.py
blob12912f21441edeaa588a90533bf0af24113d055b
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, scroll=True):
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 if scroll:
585 # scroll to the end (via idle in case the scrollbar has
586 # appeared)
587 gobject.idle_add(self.scroll_to_end)
589 def show_xep0184_warning_tooltip(self):
590 pointer = self.tv.get_pointer()
591 x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
592 pointer[0], pointer[1])
593 tags = self.tv.get_iter_at_location(x, y).get_tags()
594 tag_table = self.tv.get_buffer().get_tag_table()
595 xep0184_warning = False
596 for tag in tags:
597 if tag == tag_table.lookup('xep0184-warning'):
598 xep0184_warning = True
599 break
600 if xep0184_warning and not self.xep0184_warning_tooltip.win:
601 # check if the current pointer is still over the line
602 position = self.tv.window.get_origin()
603 self.xep0184_warning_tooltip.show_tooltip(_('This icon indicates that '
604 'this message has not yet\nbeen received by the remote end. '
605 "If this icon stays\nfor a long time, it's likely the message got "
606 'lost.'), 8, position[1] + pointer[1])
608 def show_line_tooltip(self):
609 pointer = self.tv.get_pointer()
610 x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
611 pointer[0], pointer[1])
612 tags = self.tv.get_iter_at_location(x, y).get_tags()
613 tag_table = self.tv.get_buffer().get_tag_table()
614 over_line = False
615 for tag in tags:
616 if tag == tag_table.lookup('focus-out-line'):
617 over_line = True
618 break
619 if over_line and not self.line_tooltip.win:
620 # check if the current pointer is still over the line
621 position = self.tv.window.get_origin()
622 self.line_tooltip.show_tooltip(_('Text below this line is what has '
623 'been said since the\nlast time you paid attention to this group '
624 'chat'), 8, position[1] + pointer[1])
626 def on_textview_expose_event(self, widget, event):
627 expalloc = event.area
628 exp_x0 = expalloc.x
629 exp_y0 = expalloc.y
630 exp_x1 = exp_x0 + expalloc.width
631 exp_y1 = exp_y0 + expalloc.height
633 try:
634 tryfirst = [self.image_cache[(exp_x0, exp_y0)]]
635 except KeyError:
636 tryfirst = []
638 for image in tryfirst + self.images:
639 imgalloc = image.allocation
640 img_x0 = imgalloc.x
641 img_y0 = imgalloc.y
642 img_x1 = img_x0 + imgalloc.width
643 img_y1 = img_y0 + imgalloc.height
645 if img_x0 <= exp_x0 and img_y0 <= exp_y0 and \
646 exp_x1 <= img_x1 and exp_y1 <= img_y1:
647 self.image_cache[(img_x0, img_y0)] = image
648 widget.propagate_expose(image, event)
649 return True
650 return False
652 def on_textview_motion_notify_event(self, widget, event):
654 Change the cursor to a hand when we are over a mail or an url
656 pointer_x, pointer_y = self.tv.window.get_pointer()[0:2]
657 x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
658 pointer_x, pointer_y)
659 tags = self.tv.get_iter_at_location(x, y).get_tags()
660 if self.change_cursor:
661 self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
662 gtk.gdk.Cursor(gtk.gdk.XTERM))
663 self.change_cursor = False
664 tag_table = self.tv.get_buffer().get_tag_table()
665 over_line = False
666 xep0184_warning = False
667 for tag in tags:
668 if tag in (tag_table.lookup('url'), tag_table.lookup('mail'), \
669 tag_table.lookup('xmpp'), tag_table.lookup('sth_at_sth')):
670 self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
671 gtk.gdk.Cursor(gtk.gdk.HAND2))
672 self.change_cursor = True
673 elif tag == tag_table.lookup('focus-out-line'):
674 over_line = True
675 elif tag == tag_table.lookup('xep0184-warning'):
676 xep0184_warning = True
678 if self.line_tooltip.timeout != 0:
679 # Check if we should hide the line tooltip
680 if not over_line:
681 self.line_tooltip.hide_tooltip()
682 if self.xep0184_warning_tooltip.timeout != 0:
683 # Check if we should hide the XEP-184 warning tooltip
684 if not xep0184_warning:
685 self.xep0184_warning_tooltip.hide_tooltip()
686 if over_line and not self.line_tooltip.win:
687 self.line_tooltip.timeout = gobject.timeout_add(500,
688 self.show_line_tooltip)
689 self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
690 gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
691 self.change_cursor = True
692 if xep0184_warning and not self.xep0184_warning_tooltip.win:
693 self.xep0184_warning_tooltip.timeout = gobject.timeout_add(500,
694 self.show_xep0184_warning_tooltip)
695 self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
696 gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
697 self.change_cursor = True
699 def clear(self, tv = None):
701 Clear text in the textview
703 buffer_ = self.tv.get_buffer()
704 start, end = buffer_.get_bounds()
705 buffer_.delete(start, end)
706 size = gajim.config.get('max_conversation_lines')
707 size = 2 * size - 1
708 self.marks_queue = Queue.Queue(size)
709 self.focus_out_end_mark = None
710 self.just_cleared = True
712 def visit_url_from_menuitem(self, widget, link):
714 Basically it filters out the widget instance
716 helpers.launch_browser_mailer('url', link)
718 def on_textview_populate_popup(self, textview, menu):
720 Override the default context menu and we prepend Clear (only if
721 used_in_history_window is False) and if we have sth selected we show a
722 submenu with actions on the phrase (see
723 on_conversation_textview_button_press_event)
725 separator_menuitem_was_added = False
726 if not self.used_in_history_window:
727 item = gtk.SeparatorMenuItem()
728 menu.prepend(item)
729 separator_menuitem_was_added = True
731 item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
732 menu.prepend(item)
733 id_ = item.connect('activate', self.clear)
734 self.handlers[id_] = item
736 if self.selected_phrase:
737 if not separator_menuitem_was_added:
738 item = gtk.SeparatorMenuItem()
739 menu.prepend(item)
741 if not self.used_in_history_window:
742 item = gtk.MenuItem(_('_Quote'))
743 id_ = item.connect('activate', self.on_quote)
744 self.handlers[id_] = item
745 menu.prepend(item)
747 _selected_phrase = helpers.reduce_chars_newlines(
748 self.selected_phrase, 25, 2)
749 item = gtk.MenuItem(_('_Actions for "%s"') % _selected_phrase)
750 menu.prepend(item)
751 submenu = gtk.Menu()
752 item.set_submenu(submenu)
753 phrase_for_url = urllib.quote(self.selected_phrase.encode('utf-8'))
755 always_use_en = gajim.config.get('always_english_wikipedia')
756 if always_use_en:
757 link = 'http://en.wikipedia.org/wiki/Special:Search?search=%s'\
758 % phrase_for_url
759 else:
760 link = 'http://%s.wikipedia.org/wiki/Special:Search?search=%s'\
761 % (gajim.LANG, phrase_for_url)
762 item = gtk.MenuItem(_('Read _Wikipedia Article'))
763 id_ = item.connect('activate', self.visit_url_from_menuitem, link)
764 self.handlers[id_] = item
765 submenu.append(item)
767 item = gtk.MenuItem(_('Look it up in _Dictionary'))
768 dict_link = gajim.config.get('dictionary_url')
769 if dict_link == 'WIKTIONARY':
770 # special link (yeah undocumented but default)
771 always_use_en = gajim.config.get('always_english_wiktionary')
772 if always_use_en:
773 link = 'http://en.wiktionary.org/wiki/Special:Search?search=%s'\
774 % phrase_for_url
775 else:
776 link = 'http://%s.wiktionary.org/wiki/Special:Search?search=%s'\
777 % (gajim.LANG, phrase_for_url)
778 id_ = item.connect('activate', self.visit_url_from_menuitem, link)
779 self.handlers[id_] = item
780 else:
781 if dict_link.find('%s') == -1:
782 # we must have %s in the url if not WIKTIONARY
783 item = gtk.MenuItem(_(
784 'Dictionary URL is missing an "%s" and it is not WIKTIONARY'))
785 item.set_property('sensitive', False)
786 else:
787 link = dict_link % phrase_for_url
788 id_ = item.connect('activate', self.visit_url_from_menuitem,
789 link)
790 self.handlers[id_] = item
791 submenu.append(item)
794 search_link = gajim.config.get('search_engine')
795 if search_link.find('%s') == -1:
796 # we must have %s in the url
797 item = gtk.MenuItem(_('Web Search URL is missing an "%s"'))
798 item.set_property('sensitive', False)
799 else:
800 item = gtk.MenuItem(_('Web _Search for it'))
801 link = search_link % phrase_for_url
802 id_ = item.connect('activate', self.visit_url_from_menuitem, link)
803 self.handlers[id_] = item
804 submenu.append(item)
806 item = gtk.MenuItem(_('Open as _Link'))
807 id_ = item.connect('activate', self.visit_url_from_menuitem, link)
808 self.handlers[id_] = item
809 submenu.append(item)
811 menu.show_all()
813 def on_quote(self, widget):
814 self.emit('quote', self.selected_phrase)
816 def on_textview_button_press_event(self, widget, event):
817 # If we clicked on a taged text do NOT open the standard popup menu
818 # if normal text check if we have sth selected
819 self.selected_phrase = '' # do not move belove event button check!
821 if event.button != 3: # if not right click
822 return False
824 x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
825 int(event.x), int(event.y))
826 iter_ = self.tv.get_iter_at_location(x, y)
827 tags = iter_.get_tags()
830 if tags: # we clicked on sth special (it can be status message too)
831 for tag in tags:
832 tag_name = tag.get_property('name')
833 if tag_name in ('url', 'mail', 'xmpp', 'sth_at_sth'):
834 return True # we block normal context menu
836 # we check if sth was selected and if it was we assign
837 # selected_phrase variable
838 # so on_conversation_textview_populate_popup can use it
839 buffer_ = self.tv.get_buffer()
840 return_val = buffer_.get_selection_bounds()
841 if return_val: # if sth was selected when we right-clicked
842 # get the selected text
843 start_sel, finish_sel = return_val[0], return_val[1]
844 self.selected_phrase = buffer_.get_text(start_sel, finish_sel).decode(
845 'utf-8')
846 elif ord(iter_.get_char()) > 31:
847 # we clicked on a word, do as if it's selected for context menu
848 start_sel = iter_.copy()
849 if not start_sel.starts_word():
850 start_sel.backward_word_start()
851 finish_sel = iter_.copy()
852 if not finish_sel.ends_word():
853 finish_sel.forward_word_end()
854 self.selected_phrase = buffer_.get_text(start_sel, finish_sel).decode(
855 'utf-8')
857 def on_open_link_activate(self, widget, kind, text):
858 helpers.launch_browser_mailer(kind, text)
860 def on_copy_link_activate(self, widget, text):
861 clip = gtk.clipboard_get()
862 clip.set_text(text)
864 def on_start_chat_activate(self, widget, jid):
865 gajim.interface.new_chat_from_jid(self.account, jid)
867 def on_join_group_chat_menuitem_activate(self, widget, room_jid):
868 if 'join_gc' in gajim.interface.instances[self.account]:
869 instance = gajim.interface.instances[self.account]['join_gc']
870 instance.xml.get_object('room_jid_entry').set_text(room_jid)
871 gajim.interface.instances[self.account]['join_gc'].window.present()
872 else:
873 try:
874 dialogs.JoinGroupchatWindow(account=self.account, room_jid=room_jid)
875 except GajimGeneralException:
876 pass
878 def on_add_to_roster_activate(self, widget, jid):
879 dialogs.AddNewContactWindow(self.account, jid)
881 def make_link_menu(self, event, kind, text):
882 xml = gtkgui_helpers.get_gtk_builder('chat_context_menu.ui')
883 menu = xml.get_object('chat_context_menu')
884 childs = menu.get_children()
885 if kind == 'url':
886 id_ = childs[0].connect('activate', self.on_copy_link_activate, text)
887 self.handlers[id_] = childs[0]
888 id_ = childs[1].connect('activate', self.on_open_link_activate, kind,
889 text)
890 self.handlers[id_] = childs[1]
891 childs[2].hide() # copy mail address
892 childs[3].hide() # open mail composer
893 childs[4].hide() # jid section separator
894 childs[5].hide() # start chat
895 childs[6].hide() # join group chat
896 childs[7].hide() # add to roster
897 else: # It's a mail or a JID
898 # load muc icon
899 join_group_chat_menuitem = xml.get_object('join_group_chat_menuitem')
900 muc_icon = gtkgui_helpers.load_icon('muc_active')
901 if muc_icon:
902 join_group_chat_menuitem.set_image(muc_icon)
904 text = text.lower()
905 if text.startswith('xmpp:'):
906 text = text[5:]
907 id_ = childs[2].connect('activate', self.on_copy_link_activate, text)
908 self.handlers[id_] = childs[2]
909 id_ = childs[3].connect('activate', self.on_open_link_activate, kind,
910 text)
911 self.handlers[id_] = childs[3]
912 id_ = childs[5].connect('activate', self.on_start_chat_activate, text)
913 self.handlers[id_] = childs[5]
914 id_ = childs[6].connect('activate',
915 self.on_join_group_chat_menuitem_activate, text)
916 self.handlers[id_] = childs[6]
918 if self.account and gajim.connections[self.account].\
919 roster_supported:
920 id_ = childs[7].connect('activate',
921 self.on_add_to_roster_activate, text)
922 self.handlers[id_] = childs[7]
923 childs[7].show() # show add to roster menuitem
924 else:
925 childs[7].hide() # hide add to roster menuitem
927 if kind == 'xmpp':
928 childs[2].hide() # copy mail address
929 childs[3].hide() # open mail composer
930 childs[4].hide() # jid section separator
931 elif kind == 'mail':
932 childs[4].hide() # jid section separator
933 childs[5].hide() # start chat
934 childs[6].hide() # join group chat
935 childs[7].hide() # add to roster
937 childs[0].hide() # copy link location
938 childs[1].hide() # open link in browser
940 menu.popup(None, None, None, event.button, event.time)
942 def hyperlink_handler(self, texttag, widget, event, iter_, kind):
943 if event.type == gtk.gdk.BUTTON_PRESS:
944 begin_iter = iter_.copy()
945 # we get the begining of the tag
946 while not begin_iter.begins_tag(texttag):
947 begin_iter.backward_char()
948 end_iter = iter_.copy()
949 # we get the end of the tag
950 while not end_iter.ends_tag(texttag):
951 end_iter.forward_char()
953 # Detect XHTML-IM link
954 word = getattr(texttag, 'href', None)
955 if word:
956 if word.startswith('xmpp'):
957 kind = 'xmpp'
958 elif word.startswith('mailto:'):
959 kind = 'mail'
960 elif gajim.interface.sth_at_sth_dot_sth_re.match(word):
961 # it's a JID or mail
962 kind = 'sth_at_sth'
963 else:
964 word = self.tv.get_buffer().get_text(begin_iter, end_iter).decode(
965 'utf-8')
967 if event.button == 3: # right click
968 self.make_link_menu(event, kind, word)
969 return True
970 else:
971 # we launch the correct application
972 if kind == 'xmpp':
973 word = word[5:]
974 if '?' in word:
975 (jid, action) = word.split('?')
976 if action == 'join':
977 self.on_join_group_chat_menuitem_activate(None, jid)
978 else:
979 self.on_start_chat_activate(None, jid)
980 else:
981 self.on_start_chat_activate(None, word)
982 else:
983 helpers.launch_browser_mailer(kind, word)
985 def detect_and_print_special_text(self, otext, other_tags, graphics=True):
987 Detect special text (emots & links & formatting), print normal text
988 before any special text it founds, then print special text (that happens
989 many times until last special text is printed) and then return the index
990 after *last* special text, so we can print it in
991 print_conversation_line()
993 buffer_ = self.tv.get_buffer()
995 insert_tags_func = buffer_.insert_with_tags_by_name
996 # detect_and_print_special_text() is also used by
997 # HtmlHandler.handle_specials() and there tags is gtk.TextTag objects,
998 # not strings
999 if other_tags and isinstance(other_tags[0], gtk.TextTag):
1000 insert_tags_func = buffer_.insert_with_tags
1002 index = 0
1004 # Too many special elements (emoticons, LaTeX formulas, etc)
1005 # may cause Gajim to freeze (see #5129).
1006 # We impose an arbitrary limit of 100 specials per message.
1007 specials_limit = 100
1009 # basic: links + mail + formatting is always checked (we like that)
1010 if gajim.config.get('emoticons_theme') and graphics:
1011 # search for emoticons & urls
1012 iterator = gajim.interface.emot_and_basic_re.finditer(otext)
1013 else: # search for just urls + mail + formatting
1014 iterator = gajim.interface.basic_pattern_re.finditer(otext)
1015 for match in iterator:
1016 start, end = match.span()
1017 special_text = otext[start:end]
1018 if start > index:
1019 text_before_special_text = otext[index:start]
1020 end_iter = buffer_.get_end_iter()
1021 # we insert normal text
1022 insert_tags_func(end_iter, text_before_special_text, *other_tags)
1023 index = end # update index
1025 # now print it
1026 self.print_special_text(special_text, other_tags, graphics=graphics)
1027 specials_limit -= 1
1028 if specials_limit <= 0:
1029 break
1031 # add the rest of text located in the index and after
1032 end_iter = buffer_.get_end_iter()
1033 insert_tags_func(end_iter, otext[index:], *other_tags)
1035 return buffer_.get_end_iter()
1037 def print_special_text(self, special_text, other_tags, graphics=True):
1039 Is called by detect_and_print_special_text and prints special text
1040 (emots, links, formatting)
1042 tags = []
1043 use_other_tags = True
1044 text_is_valid_uri = False
1045 is_xhtml_link = None
1046 show_ascii_formatting_chars = \
1047 gajim.config.get('show_ascii_formatting_chars')
1048 buffer_ = self.tv.get_buffer()
1050 # Detect XHTML-IM link
1051 ttt = buffer_.get_tag_table()
1052 tags_ = [(ttt.lookup(t) if isinstance(t, str) else t) for t in other_tags]
1053 for t in tags_:
1054 is_xhtml_link = getattr(t, 'href', None)
1055 if is_xhtml_link:
1056 break
1058 # Check if we accept this as an uri
1059 schemes = gajim.config.get('uri_schemes').split()
1060 for scheme in schemes:
1061 if special_text.startswith(scheme):
1062 text_is_valid_uri = True
1064 possible_emot_ascii_caps = special_text.upper() # emoticons keys are CAPS
1065 if gajim.config.get('emoticons_theme') and \
1066 possible_emot_ascii_caps in gajim.interface.emoticons.keys() and graphics:
1067 # it's an emoticon
1068 emot_ascii = possible_emot_ascii_caps
1069 end_iter = buffer_.get_end_iter()
1070 anchor = buffer_.create_child_anchor(end_iter)
1071 img = TextViewImage(anchor, special_text)
1072 animations = gajim.interface.emoticons_animations
1073 if not emot_ascii in animations:
1074 animations[emot_ascii] = gtk.gdk.PixbufAnimation(
1075 gajim.interface.emoticons[emot_ascii])
1076 img.set_from_animation(animations[emot_ascii])
1077 img.show()
1078 self.images.append(img)
1079 # add with possible animation
1080 self.tv.add_child_at_anchor(img, anchor)
1081 elif special_text.startswith('www.') or \
1082 special_text.startswith('ftp.') or \
1083 text_is_valid_uri and not is_xhtml_link:
1084 tags.append('url')
1085 elif special_text.startswith('mailto:') and not is_xhtml_link:
1086 tags.append('mail')
1087 elif special_text.startswith('xmpp:') and not is_xhtml_link:
1088 tags.append('xmpp')
1089 elif gajim.interface.sth_at_sth_dot_sth_re.match(special_text) and\
1090 not is_xhtml_link:
1091 # it's a JID or mail
1092 tags.append('sth_at_sth')
1093 elif special_text.startswith('*'): # it's a bold text
1094 tags.append('bold')
1095 if special_text[1] == '/' and special_text[-2] == '/' and\
1096 len(special_text) > 4: # it's also italic
1097 tags.append('italic')
1098 if not show_ascii_formatting_chars:
1099 special_text = special_text[2:-2] # remove */ /*
1100 elif special_text[1] == '_' and special_text[-2] == '_' and \
1101 len(special_text) > 4: # it's also underlined
1102 tags.append('underline')
1103 if not show_ascii_formatting_chars:
1104 special_text = special_text[2:-2] # remove *_ _*
1105 else:
1106 if not show_ascii_formatting_chars:
1107 special_text = special_text[1:-1] # remove * *
1108 elif special_text.startswith('/'): # it's an italic text
1109 tags.append('italic')
1110 if special_text[1] == '*' and special_text[-2] == '*' and \
1111 len(special_text) > 4: # it's also bold
1112 tags.append('bold')
1113 if not show_ascii_formatting_chars:
1114 special_text = special_text[2:-2] # remove /* */
1115 elif special_text[1] == '_' and special_text[-2] == '_' and \
1116 len(special_text) > 4: # it's also underlined
1117 tags.append('underline')
1118 if not show_ascii_formatting_chars:
1119 special_text = special_text[2:-2] # remove /_ _/
1120 else:
1121 if not show_ascii_formatting_chars:
1122 special_text = special_text[1:-1] # remove / /
1123 elif special_text.startswith('_'): # it's an underlined text
1124 tags.append('underline')
1125 if special_text[1] == '*' and special_text[-2] == '*' and \
1126 len(special_text) > 4: # it's also bold
1127 tags.append('bold')
1128 if not show_ascii_formatting_chars:
1129 special_text = special_text[2:-2] # remove _* *_
1130 elif special_text[1] == '/' and special_text[-2] == '/' and \
1131 len(special_text) > 4: # it's also italic
1132 tags.append('italic')
1133 if not show_ascii_formatting_chars:
1134 special_text = special_text[2:-2] # remove _/ /_
1135 else:
1136 if not show_ascii_formatting_chars:
1137 special_text = special_text[1:-1] # remove _ _
1138 elif gajim.HAVE_LATEX and special_text.startswith('$$') and \
1139 special_text.endswith('$$') and graphics:
1140 try:
1141 imagepath = latex.latex_to_image(special_text[2:-2])
1142 except LatexError, e:
1143 # print the error after the line has been written
1144 gobject.idle_add(self.print_conversation_line, str(e), '', 'info',
1145 '', None)
1146 imagepath = None
1147 end_iter = buffer_.get_end_iter()
1148 if imagepath is not None:
1149 anchor = buffer_.create_child_anchor(end_iter)
1150 img = TextViewImage(anchor, special_text)
1151 img.set_from_file(imagepath)
1152 img.show()
1153 # add
1154 self.tv.add_child_at_anchor(img, anchor)
1155 # delete old file
1156 try:
1157 os.remove(imagepath)
1158 except Exception:
1159 pass
1160 else:
1161 buffer_.insert(end_iter, special_text)
1162 use_other_tags = False
1163 else:
1164 # It's nothing special
1165 if use_other_tags:
1166 end_iter = buffer_.get_end_iter()
1167 insert_tags_func = buffer_.insert_with_tags_by_name
1168 if other_tags and isinstance(other_tags[0], gtk.TextTag):
1169 insert_tags_func = buffer_.insert_with_tags
1171 insert_tags_func(end_iter, special_text, *other_tags)
1173 if tags:
1174 end_iter = buffer_.get_end_iter()
1175 all_tags = tags[:]
1176 if use_other_tags:
1177 all_tags += other_tags
1178 # convert all names to TextTag
1179 all_tags = [(ttt.lookup(t) if isinstance(t, str) else t) for t in all_tags]
1180 buffer_.insert_with_tags(end_iter, special_text, *all_tags)
1182 def print_empty_line(self):
1183 buffer_ = self.tv.get_buffer()
1184 end_iter = buffer_.get_end_iter()
1185 buffer_.insert_with_tags_by_name(end_iter, '\n', 'eol')
1186 self.just_cleared = False
1188 def print_conversation_line(self, text, jid, kind, name, tim,
1189 other_tags_for_name=[], other_tags_for_time=[],
1190 other_tags_for_text=[], subject=None, old_kind=None, xhtml=None,
1191 simple=False, graphics=True, displaymarking=None):
1193 Print 'chat' type messages
1195 buffer_ = self.tv.get_buffer()
1196 buffer_.begin_user_action()
1197 if self.marks_queue.full():
1198 # remove oldest line
1199 m1 = self.marks_queue.get()
1200 m2 = self.marks_queue.get()
1201 i1 = buffer_.get_iter_at_mark(m1)
1202 i2 = buffer_.get_iter_at_mark(m2)
1203 buffer_.delete(i1, i2)
1204 buffer_.delete_mark(m1)
1205 end_iter = buffer_.get_end_iter()
1206 end_offset = end_iter.get_offset()
1207 at_the_end = self.at_the_end()
1208 move_selection = False
1209 if buffer_.get_has_selection() and buffer_.get_selection_bounds()[1].\
1210 get_offset() == end_offset:
1211 move_selection = True
1213 # Create one mark and add it to queue once if it's the first line
1214 # else twice (one for end bound, one for start bound)
1215 mark = None
1216 if buffer_.get_char_count() > 0:
1217 if not simple:
1218 buffer_.insert_with_tags_by_name(end_iter, '\n', 'eol')
1219 if move_selection:
1220 sel_start, sel_end = buffer_.get_selection_bounds()
1221 sel_end.backward_char()
1222 buffer_.select_range(sel_start, sel_end)
1223 mark = buffer_.create_mark(None, end_iter, left_gravity=True)
1224 self.marks_queue.put(mark)
1225 if not mark:
1226 mark = buffer_.create_mark(None, end_iter, left_gravity=True)
1227 self.marks_queue.put(mark)
1228 if kind == 'incoming_queue':
1229 kind = 'incoming'
1230 if old_kind == 'incoming_queue':
1231 old_kind = 'incoming'
1232 # print the time stamp
1233 if not tim:
1234 # We don't have tim for outgoing messages...
1235 tim = time.localtime()
1236 current_print_time = gajim.config.get('print_time')
1237 if current_print_time == 'always' and kind != 'info' and not simple:
1238 timestamp_str = self.get_time_to_show(tim)
1239 timestamp = time.strftime(timestamp_str, tim)
1240 buffer_.insert_with_tags_by_name(end_iter, timestamp,
1241 *other_tags_for_time)
1242 elif current_print_time == 'sometimes' and kind != 'info' and not simple:
1243 every_foo_seconds = 60 * gajim.config.get(
1244 'print_ichat_every_foo_minutes')
1245 seconds_passed = time.mktime(tim) - self.last_time_printout
1246 if seconds_passed > every_foo_seconds:
1247 self.last_time_printout = time.mktime(tim)
1248 end_iter = buffer_.get_end_iter()
1249 if gajim.config.get('print_time_fuzzy') > 0:
1250 ft = self.fc.fuzzy_time(gajim.config.get('print_time_fuzzy'), tim)
1251 tim_format = ft.decode(locale.getpreferredencoding())
1252 else:
1253 tim_format = self.get_time_to_show(tim)
1254 buffer_.insert_with_tags_by_name(end_iter, tim_format + '\n',
1255 'time_sometimes')
1256 # If there's a displaymarking, print it here.
1257 if displaymarking:
1258 self.print_displaymarking(displaymarking)
1259 # kind = info, we print things as if it was a status: same color, ...
1260 if kind in ('error', 'info'):
1261 kind = 'status'
1262 other_text_tag = self.detect_other_text_tag(text, kind)
1263 text_tags = other_tags_for_text[:] # create a new list
1264 if other_text_tag:
1265 # note that color of /me may be overwritten in gc_control
1266 text_tags.append(other_text_tag)
1267 else: # not status nor /me
1268 if gajim.config.get('chat_merge_consecutive_nickname'):
1269 if kind != old_kind or self.just_cleared:
1270 self.print_name(name, kind, other_tags_for_name)
1271 else:
1272 self.print_real_text(gajim.config.get(
1273 'chat_merge_consecutive_nickname_indent'))
1274 else:
1275 self.print_name(name, kind, other_tags_for_name)
1276 if kind == 'incoming':
1277 text_tags.append('incomingtxt')
1278 elif kind == 'outgoing':
1279 text_tags.append('outgoingtxt')
1280 self.print_subject(subject)
1281 self.print_real_text(text, text_tags, name, xhtml, graphics=graphics)
1283 # scroll to the end of the textview
1284 if at_the_end or kind == 'outgoing':
1285 # we are at the end or we are sending something
1286 # scroll to the end (via idle in case the scrollbar has appeared)
1287 if gajim.config.get('use_smooth_scrolling'):
1288 gobject.idle_add(self.smooth_scroll_to_end)
1289 else:
1290 gobject.idle_add(self.scroll_to_end)
1292 self.just_cleared = False
1293 buffer_.end_user_action()
1295 def get_time_to_show(self, tim):
1297 Get the time, with the day before if needed and return it. It DOESN'T
1298 format a fuzzy time
1300 format_ = ''
1301 # get difference in days since epoch (86400 = 24*3600)
1302 # number of days since epoch for current time (in GMT) -
1303 # number of days since epoch for message (in GMT)
1304 diff_day = int(timegm(time.localtime())) / 86400 -\
1305 int(timegm(tim)) / 86400
1306 if diff_day == 0:
1307 day_str = ''
1308 else:
1309 #%i is day in year (1-365)
1310 day_str = i18n.ngettext('Yesterday',
1311 '%(nb_days)i days ago', diff_day, {'nb_days': diff_day},
1312 {'nb_days': diff_day})
1313 if day_str:
1314 format_ += day_str + ' '
1315 timestamp_str = gajim.config.get('time_stamp')
1316 timestamp_str = helpers.from_one_line(timestamp_str)
1317 format_ += timestamp_str
1318 tim_format = time.strftime(format_, tim)
1319 if locale.getpreferredencoding() != 'KOI8-R':
1320 # if tim_format comes as unicode because of day_str.
1321 # we convert it to the encoding that we want (and that is utf-8)
1322 tim_format = helpers.ensure_utf8_string(tim_format)
1323 return tim_format
1325 def detect_other_text_tag(self, text, kind):
1326 if kind == 'status':
1327 return kind
1328 elif text.startswith('/me ') or text.startswith('/me\n'):
1329 return kind
1331 def print_displaymarking(self, displaymarking):
1332 bgcolor = displaymarking.getAttr('bgcolor') or '#FFF'
1333 fgcolor = displaymarking.getAttr('fgcolor') or '#000'
1334 text = displaymarking.getData()
1335 if text:
1336 buffer_ = self.tv.get_buffer()
1337 end_iter = buffer_.get_end_iter()
1338 tag = self.displaymarking_tags.setdefault(bgcolor + '/' + fgcolor,
1339 buffer_.create_tag(None, background=bgcolor, foreground=fgcolor))
1340 buffer_.insert_with_tags(end_iter, '[' + text + ']', tag)
1341 end_iter = buffer_.get_end_iter()
1342 buffer_.insert_with_tags(end_iter, ' ')
1344 def print_name(self, name, kind, other_tags_for_name):
1345 if name:
1346 buffer_ = self.tv.get_buffer()
1347 end_iter = buffer_.get_end_iter()
1348 name_tags = other_tags_for_name[:] # create a new list
1349 name_tags.append(kind)
1350 before_str = gajim.config.get('before_nickname')
1351 before_str = helpers.from_one_line(before_str)
1352 after_str = gajim.config.get('after_nickname')
1353 after_str = helpers.from_one_line(after_str)
1354 format = before_str + name + after_str + ' '
1355 buffer_.insert_with_tags_by_name(end_iter, format, *name_tags)
1357 def print_subject(self, subject):
1358 if subject: # if we have subject, show it too!
1359 subject = _('Subject: %s\n') % subject
1360 buffer_ = self.tv.get_buffer()
1361 end_iter = buffer_.get_end_iter()
1362 buffer_.insert(end_iter, subject)
1363 self.print_empty_line()
1365 def print_real_text(self, text, text_tags=[], name=None, xhtml=None,
1366 graphics=True):
1368 Add normal and special text. call this to add text
1370 if xhtml:
1371 try:
1372 if name and (text.startswith('/me ') or text.startswith('/me\n')):
1373 xhtml = xhtml.replace('/me', '<i>* %s</i>' % (name,), 1)
1374 self.tv.display_html(xhtml.encode('utf-8'), self)
1375 return
1376 except Exception, e:
1377 gajim.log.debug('Error processing xhtml' + str(e))
1378 gajim.log.debug('with |' + xhtml + '|')
1380 # /me is replaced by name if name is given
1381 if name and (text.startswith('/me ') or text.startswith('/me\n')):
1382 text = '* ' + name + text[3:]
1383 text_tags.append('italic')
1384 # detect urls formatting and if the user has it on emoticons
1385 self.detect_and_print_special_text(text, text_tags, graphics=graphics)