|
Module conversation_textview
|
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 from threading import Timer
31
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
42
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
50
51 from htmltextview import HtmlTextView
52 from common.exceptions import GajimGeneralException
53 from common.exceptions import LatexError
54
55 NOT_SHOWN = 0
56 ALREADY_RECEIVED = 1
57 SHOWN = 2
58
60 name = mark.get_name()
61 if name and name in ('selection_bound', 'insert'):
62 return True
63 else:
64 return False
65
67 return widget.flags() & gtk.HAS_FOCUS == gtk.HAS_FOCUS
68
69 -class TextViewImage(gtk.Image):
70
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)
80
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
91
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
103
105 selected = self._get_selected()
106 if self._selected != selected:
107 self._selected = selected
108 self.queue_draw()
109
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)
115
117 for func in self._disconnect_funcs:
118 func()
119 self._disconnect_funcs = []
120
121 - def on_parent_set(self, widget, old_parent):
122 parent = self.get_parent()
123 if not parent:
124 self._disconnect_signals()
125 return
126
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)
130
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)
134
135 - def do_queue_draw(self, *args):
136 self.queue_draw()
137 return False
138
139 - def on_mark_set(self, buf, iterat, mark):
140 self.on_mark_modified(mark)
141 return False
142
143 - def on_mark_deleted(self, buf, mark):
144 self.on_mark_modified(mark)
145 return False
146
147 - def on_mark_modified(self, mark):
150
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
159
160
161 -class ConversationTextview(gobject.GObject):
162 """
163 Class for the conversation textview (where user reads already said messages)
164 for chat/groupchat windows
165 """
166 __gsignals__ = dict(
167 quote = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION,
168 None,
169 (str, )
170 )
171 )
172
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')
176
177
178 MAX_SCROLL_TIME = 0.4
179 SCROLL_DELAY = 33
180
181 - def __init__(self, account, used_in_history_window = False):
182 """
183 If used_in_history_window is True, then we do not show Clear menuitem in
184 context menu
185 """
186 gobject.GObject.__init__(self)
187 self.used_in_history_window = used_in_history_window
188
189 self.fc = FuzzyClock()
190
191
192
193 self.tv = HtmlTextView()
194 self.tv.html_hyperlink_handler = self.html_hyperlink_handler
195
196
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 = {}
209
210
211 self.auto_scrolling = False
212
213
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
222
223 id_ = self.tv.connect('expose-event',
224 self.on_textview_expose_event)
225 self.handlers[id_] = self.tv
226
227
228 self.account = account
229 self.change_cursor = False
230 self.last_time_printout = 0
231
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)
237
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)
243
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)
249
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)
255
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)
262
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)
269
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)
276
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)
281
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)
286
287 tag = buffer_.create_tag('small')
288 tag.set_property('scale', pango.SCALE_SMALL)
289
290 tag = buffer_.create_tag('restored_message')
291 color = gajim.config.get('restored_messages_color')
292 tag.set_property('foreground', color)
293
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
300
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
306
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
312
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
319
320 tag = buffer_.create_tag('bold')
321 tag.set_property('weight', pango.WEIGHT_BOLD)
322
323 tag = buffer_.create_tag('italic')
324 tag.set_property('style', pango.STYLE_ITALIC)
325
326 tag = buffer_.create_tag('underline')
327 tag.set_property('underline', pango.UNDERLINE_SINGLE)
328
329 buffer_.create_tag('focus-out-line', justification = gtk.JUSTIFY_CENTER)
330 self.displaymarking_tags = {}
331
332 tag = buffer_.create_tag('xep0184-warning')
333
334
335 size = gajim.config.get('max_conversation_lines')
336 size = 2 * size - 1
337 self.marks_queue = Queue.Queue(size)
338
339 self.allow_focus_out_line = True
340
341 self.focus_out_end_mark = None
342
343 self.xep0184_warning_tooltip = tooltips.BaseTooltip()
344
345 self.line_tooltip = tooltips.BaseTooltip()
346
347 self.tv.focus_out_line_pixbuf = ConversationTextview.FOCUS_OUT_LINE_PIXBUF
348 self.smooth_id = None
349
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
357
358
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'))
366
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
375
376
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
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
394
398
400 if not self.smooth_id:
401
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
411
421
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
434
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
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)
448
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
456
458 if self.smooth_id:
459 gobject.source_remove(self.smooth_id)
460 self.smooth_id = None
461 self.smooth_scroll_timer.cancel()
462
463 - def show_xep0184_warning(self, id_):
464 if id_ in self.xep0184_marks:
465 return
466
467 buffer_ = self.tv.get_buffer()
468 buffer_.begin_user_action()
469
470 self.xep0184_marks[id_] = buffer_.create_mark(None,
471 buffer_.get_end_iter(), left_gravity=True)
472 self.xep0184_shown[id_] = NOT_SHOWN
473
474 def show_it():
475 if (not id_ in self.xep0184_shown) or \
476 self.xep0184_shown[id_] == ALREADY_RECEIVED:
477 return False
478
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)
492
493 self.xep0184_shown[id_] = SHOWN
494 return False
495 gobject.timeout_add_seconds(3, show_it)
496
497 buffer_.end_user_action()
498
499 - def hide_xep0184_warning(self, id_):
500 if id_ not in self.xep0184_marks:
501 return
502
503 if self.xep0184_shown[id_] == NOT_SHOWN:
504 self.xep0184_shown[id_] = ALREADY_RECEIVED
505 return
506
507 buffer_ = self.tv.get_buffer()
508 buffer_.begin_user_action()
509
510 begin_iter = buffer_.get_iter_at_mark(self.xep0184_marks[id_])
511
512 end_iter = begin_iter.copy()
513
514 end_iter.forward_char()
515 end_iter.forward_char()
516
517 buffer_.delete(begin_iter, end_iter)
518 buffer_.delete_mark(self.xep0184_marks[id_])
519
520 buffer_.end_user_action()
521
522 del self.xep0184_marks[id_]
523 del self.xep0184_shown[id_]
524
526 if not self.allow_focus_out_line:
527
528
529 return
530
531 print_focus_out_line = False
532 buffer_ = self.tv.get_buffer()
533
534 if self.focus_out_end_mark is None:
535
536 print_focus_out_line = True
537
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
543
544
545
546 print_focus_out_line = True
547
548 if print_focus_out_line and buffer_.get_char_count() > 0:
549 buffer_.begin_user_action()
550
551
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
557 begin_iter_for_previous_line.backward_chars(2)
558
559
560 buffer_.delete(begin_iter_for_previous_line,
561 end_iter_for_previous_line)
562 buffer_.delete_mark(self.focus_out_end_mark)
563
564
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)
569
570 end_iter = buffer_.get_end_iter()
571 before_img_iter = end_iter.copy()
572
573 before_img_iter.backward_char()
574 buffer_.apply_tag_by_name('focus-out-line', before_img_iter, end_iter)
575
576 self.allow_focus_out_line = False
577
578
579 self.focus_out_end_mark = buffer_.create_mark(None,
580 buffer_.get_end_iter(), left_gravity=True)
581
582 buffer_.end_user_action()
583
584
585 gobject.idle_add(self.scroll_to_end)
586
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
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])
605
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
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])
623
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
630
631 try:
632 tryfirst = [self.image_cache[(exp_x0, exp_y0)]]
633 except KeyError:
634 tryfirst = []
635
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
642
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
649
651 """
652 Change the cursor to a hand when we are over a mail or an url
653 """
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
675
676 if self.line_tooltip.timeout != 0:
677
678 if not over_line:
679 self.line_tooltip.hide_tooltip()
680 if self.xep0184_warning_tooltip.timeout != 0:
681
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
696
697 - def clear(self, tv = None):
698 """
699 Clear text in the textview
700 """
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
709
711 """
712 Basically it filters out the widget instance
713 """
714 helpers.launch_browser_mailer('url', link)
715
717 """
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)
722 """
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
728
729 item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
730 menu.prepend(item)
731 id_ = item.connect('activate', self.clear)
732 self.handlers[id_] = item
733
734 if self.selected_phrase:
735 if not separator_menuitem_was_added:
736 item = gtk.SeparatorMenuItem()
737 menu.prepend(item)
738
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)
744
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'))
752
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)
764
765 item = gtk.MenuItem(_('Look it up in _Dictionary'))
766 dict_link = gajim.config.get('dictionary_url')
767 if dict_link == 'WIKTIONARY':
768
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
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)
790
791
792 search_link = gajim.config.get('search_engine')
793 if search_link.find('%s') == -1:
794
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)
803
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)
808
809 menu.show_all()
810
811 - def on_quote(self, widget):
812 self.emit('quote', self.selected_phrase)
813
815
816
817 self.selected_phrase = ''
818
819 if event.button != 3:
820 return False
821
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()
826
827
828 if tags:
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
833
834
835
836
837 buffer_ = self.tv.get_buffer()
838 return_val = buffer_.get_selection_bounds()
839 if return_val:
840
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
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')
854
855 - def on_open_link_activate(self, widget, kind, text):
857
858 - def on_copy_link_activate(self, widget, text):
859 clip = gtk.clipboard_get()
860 clip.set_text(text)
861
862 - def on_start_chat_activate(self, widget, jid):
864
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
875
876 - def on_add_to_roster_activate(self, widget, jid):
878
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()
890 childs[3].hide()
891 childs[4].hide()
892 childs[5].hide()
893 childs[6].hide()
894 childs[7].hide()
895 else:
896
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)
901
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]
915
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()
921 else:
922 childs[7].hide()
923
924 if kind == 'xmpp':
925 childs[2].hide()
926 childs[3].hide()
927 childs[4].hide()
928 elif kind == 'mail':
929 childs[4].hide()
930 childs[5].hide()
931 childs[6].hide()
932 childs[7].hide()
933
934 childs[0].hide()
935 childs[1].hide()
936
937 menu.popup(None, None, None, event.button, event.time)
938
939 - def hyperlink_handler(self, texttag, widget, event, iter_, kind):
940 if event.type == gtk.gdk.BUTTON_PRESS:
941 begin_iter = iter_.copy()
942
943 while not begin_iter.begins_tag(texttag):
944 begin_iter.backward_char()
945 end_iter = iter_.copy()
946
947 while not end_iter.ends_tag(texttag):
948 end_iter.forward_char()
949 word = self.tv.get_buffer().get_text(begin_iter, end_iter).decode(
950 'utf-8')
951 if event.button == 3:
952 self.make_link_menu(event, kind, word)
953 else:
954
955 if kind == 'xmpp':
956 word = word[5:]
957 if '?' in word:
958 (jid, action) = word.split('?')
959 if action == 'join':
960 self.on_join_group_chat_menuitem_activate(None, jid)
961 else:
962 self.on_start_chat_activate(None, jid)
963 else:
964 self.on_start_chat_activate(None, word)
965 else:
966 helpers.launch_browser_mailer(kind, word)
967
968 - def html_hyperlink_handler(self, texttag, widget, event, iter_, kind, href):
969 if event.type == gtk.gdk.BUTTON_PRESS:
970 if event.button == 3:
971 self.make_link_menu(event, kind, href)
972 return True
973 else:
974
975 helpers.launch_browser_mailer(kind, href)
976
977
978 - def detect_and_print_special_text(self, otext, other_tags, graphics=True):
979 """
980 Detect special text (emots & links & formatting), print normal text
981 before any special text it founds, then print special text (that happens
982 many times until last special text is printed) and then return the index
983 after *last* special text, so we can print it in
984 print_conversation_line()
985 """
986 buffer_ = self.tv.get_buffer()
987
988 insert_tags_func = buffer_.insert_with_tags_by_name
989
990
991
992 if other_tags and isinstance(other_tags[0], gtk.TextTag):
993 insert_tags_func = buffer_.insert_with_tags
994
995 index = 0
996
997
998
999
1000 specials_limit = 100
1001
1002
1003 if gajim.config.get('emoticons_theme') and graphics:
1004
1005 iterator = gajim.interface.emot_and_basic_re.finditer(otext)
1006 else:
1007 iterator = gajim.interface.basic_pattern_re.finditer(otext)
1008 for match in iterator:
1009 start, end = match.span()
1010 special_text = otext[start:end]
1011 if start > index:
1012 text_before_special_text = otext[index:start]
1013 end_iter = buffer_.get_end_iter()
1014
1015 insert_tags_func(end_iter, text_before_special_text, *other_tags)
1016 index = end
1017
1018
1019 self.print_special_text(special_text, other_tags, graphics=graphics)
1020 specials_limit -= 1
1021 if specials_limit <= 0:
1022 break
1023
1024
1025 end_iter = buffer_.get_end_iter()
1026 insert_tags_func(end_iter, otext[index:], *other_tags)
1027
1028 return buffer_.get_end_iter()
1029
1030 - def print_special_text(self, special_text, other_tags, graphics=True):
1031 """
1032 Is called by detect_and_print_special_text and prints special text
1033 (emots, links, formatting)
1034 """
1035 tags = []
1036 use_other_tags = True
1037 text_is_valid_uri = False
1038 show_ascii_formatting_chars = \
1039 gajim.config.get('show_ascii_formatting_chars')
1040 buffer_ = self.tv.get_buffer()
1041
1042
1043 schemes = gajim.config.get('uri_schemes').split()
1044 for scheme in schemes:
1045 if special_text.startswith(scheme + ':'):
1046 text_is_valid_uri = True
1047
1048 possible_emot_ascii_caps = special_text.upper()
1049 if gajim.config.get('emoticons_theme') and \
1050 possible_emot_ascii_caps in gajim.interface.emoticons.keys() and graphics:
1051
1052 emot_ascii = possible_emot_ascii_caps
1053 end_iter = buffer_.get_end_iter()
1054 anchor = buffer_.create_child_anchor(end_iter)
1055 img = TextViewImage(anchor, special_text)
1056 animations = gajim.interface.emoticons_animations
1057 if not emot_ascii in animations:
1058 animations[emot_ascii] = gtk.gdk.PixbufAnimation(
1059 gajim.interface.emoticons[emot_ascii])
1060 img.set_from_animation(animations[emot_ascii])
1061 img.show()
1062 self.images.append(img)
1063
1064 self.tv.add_child_at_anchor(img, anchor)
1065 elif special_text.startswith('www.') or \
1066 special_text.startswith('ftp.') or \
1067 text_is_valid_uri:
1068 tags.append('url')
1069 use_other_tags = False
1070 elif special_text.startswith('mailto:'):
1071 tags.append('mail')
1072 use_other_tags = False
1073 elif special_text.startswith('xmpp:'):
1074 tags.append('xmpp')
1075 use_other_tags = False
1076 elif gajim.interface.sth_at_sth_dot_sth_re.match(special_text):
1077
1078 tags.append('sth_at_sth')
1079 use_other_tags = False
1080 elif special_text.startswith('*'):
1081 tags.append('bold')
1082 if special_text[1] == '/' and special_text[-2] == '/' and\
1083 len(special_text) > 4:
1084 tags.append('italic')
1085 if not show_ascii_formatting_chars:
1086 special_text = special_text[2:-2]
1087 elif special_text[1] == '_' and special_text[-2] == '_' and \
1088 len(special_text) > 4:
1089 tags.append('underline')
1090 if not show_ascii_formatting_chars:
1091 special_text = special_text[2:-2]
1092 else:
1093 if not show_ascii_formatting_chars:
1094 special_text = special_text[1:-1]
1095 elif special_text.startswith('/'):
1096 tags.append('italic')
1097 if special_text[1] == '*' and special_text[-2] == '*' and \
1098 len(special_text) > 4:
1099 tags.append('bold')
1100 if not show_ascii_formatting_chars:
1101 special_text = special_text[2:-2]
1102 elif special_text[1] == '_' and special_text[-2] == '_' and \
1103 len(special_text) > 4:
1104 tags.append('underline')
1105 if not show_ascii_formatting_chars:
1106 special_text = special_text[2:-2]
1107 else:
1108 if not show_ascii_formatting_chars:
1109 special_text = special_text[1:-1]
1110 elif special_text.startswith('_'):
1111 tags.append('underline')
1112 if special_text[1] == '*' and special_text[-2] == '*' and \
1113 len(special_text) > 4:
1114 tags.append('bold')
1115 if not show_ascii_formatting_chars:
1116 special_text = special_text[2:-2]
1117 elif special_text[1] == '/' and special_text[-2] == '/' and \
1118 len(special_text) > 4:
1119 tags.append('italic')
1120 if not show_ascii_formatting_chars:
1121 special_text = special_text[2:-2]
1122 else:
1123 if not show_ascii_formatting_chars:
1124 special_text = special_text[1:-1]
1125 elif gajim.HAVE_LATEX and special_text.startswith('$$') and \
1126 special_text.endswith('$$') and graphics:
1127 try:
1128 imagepath = latex.latex_to_image(special_text[2:-2])
1129 except LatexError, e:
1130
1131 gobject.idle_add(self.print_conversation_line, str(e), '', 'info',
1132 '', None)
1133 imagepath = None
1134 end_iter = buffer_.get_end_iter()
1135 if imagepath is not None:
1136 anchor = buffer_.create_child_anchor(end_iter)
1137 img = TextViewImage(anchor, special_text)
1138 img.set_from_file(imagepath)
1139 img.show()
1140
1141 self.tv.add_child_at_anchor(img, anchor)
1142
1143 try:
1144 os.remove(imagepath)
1145 except Exception:
1146 pass
1147 else:
1148 buffer_.insert(end_iter, special_text)
1149 use_other_tags = False
1150 else:
1151
1152 if use_other_tags:
1153 end_iter = buffer_.get_end_iter()
1154 insert_tags_func = buffer_.insert_with_tags_by_name
1155 if other_tags and isinstance(other_tags[0], gtk.TextTag):
1156 insert_tags_func = buffer_.insert_with_tags
1157
1158 insert_tags_func(end_iter, special_text, *other_tags)
1159
1160 if tags:
1161 end_iter = buffer_.get_end_iter()
1162 all_tags = tags[:]
1163 if use_other_tags:
1164 all_tags += other_tags
1165
1166 ttt = buffer_.get_tag_table()
1167 all_tags = [(ttt.lookup(t) if isinstance(t, str) else t) for t in all_tags]
1168 buffer_.insert_with_tags(end_iter, special_text, *all_tags)
1169
1170 - def print_empty_line(self):
1171 buffer_ = self.tv.get_buffer()
1172 end_iter = buffer_.get_end_iter()
1173 buffer_.insert_with_tags_by_name(end_iter, '\n', 'eol')
1174 self.just_cleared = False
1175
1176 - def print_conversation_line(self, text, jid, kind, name, tim,
1177 other_tags_for_name=[], other_tags_for_time=[],
1178 other_tags_for_text=[], subject=None, old_kind=None, xhtml=None,
1179 simple=False, graphics=True, displaymarking=None):
1180 """
1181 Print 'chat' type messages
1182 """
1183 buffer_ = self.tv.get_buffer()
1184 buffer_.begin_user_action()
1185 if self.marks_queue.full():
1186
1187 m1 = self.marks_queue.get()
1188 m2 = self.marks_queue.get()
1189 i1 = buffer_.get_iter_at_mark(m1)
1190 i2 = buffer_.get_iter_at_mark(m2)
1191 buffer_.delete(i1, i2)
1192 buffer_.delete_mark(m1)
1193 end_iter = buffer_.get_end_iter()
1194 end_offset = end_iter.get_offset()
1195 at_the_end = self.at_the_end()
1196 move_selection = False
1197 if buffer_.get_has_selection() and buffer_.get_selection_bounds()[1].\
1198 get_offset() == end_offset:
1199 move_selection = True
1200
1201
1202
1203 mark = None
1204 if buffer_.get_char_count() > 0:
1205 if not simple:
1206 buffer_.insert_with_tags_by_name(end_iter, '\n', 'eol')
1207 if move_selection:
1208 sel_start, sel_end = buffer_.get_selection_bounds()
1209 sel_end.backward_char()
1210 buffer_.select_range(sel_start, sel_end)
1211 mark = buffer_.create_mark(None, end_iter, left_gravity=True)
1212 self.marks_queue.put(mark)
1213 if not mark:
1214 mark = buffer_.create_mark(None, end_iter, left_gravity=True)
1215 self.marks_queue.put(mark)
1216 if kind == 'incoming_queue':
1217 kind = 'incoming'
1218 if old_kind == 'incoming_queue':
1219 old_kind = 'incoming'
1220
1221 if not tim:
1222
1223 tim = time.localtime()
1224 current_print_time = gajim.config.get('print_time')
1225 if current_print_time == 'always' and kind != 'info' and not simple:
1226 timestamp_str = self.get_time_to_show(tim)
1227 timestamp = time.strftime(timestamp_str, tim)
1228 buffer_.insert_with_tags_by_name(end_iter, timestamp,
1229 *other_tags_for_time)
1230 elif current_print_time == 'sometimes' and kind != 'info' and not simple:
1231 every_foo_seconds = 60 * gajim.config.get(
1232 'print_ichat_every_foo_minutes')
1233 seconds_passed = time.mktime(tim) - self.last_time_printout
1234 if seconds_passed > every_foo_seconds:
1235 self.last_time_printout = time.mktime(tim)
1236 end_iter = buffer_.get_end_iter()
1237 if gajim.config.get('print_time_fuzzy') > 0:
1238 ft = self.fc.fuzzy_time(gajim.config.get('print_time_fuzzy'), tim)
1239 tim_format = ft.decode(locale.getpreferredencoding())
1240 else:
1241 tim_format = self.get_time_to_show(tim)
1242 buffer_.insert_with_tags_by_name(end_iter, tim_format + '\n',
1243 'time_sometimes')
1244
1245 if displaymarking:
1246 self.print_displaymarking(displaymarking)
1247
1248 if kind in ('error', 'info'):
1249 kind = 'status'
1250 other_text_tag = self.detect_other_text_tag(text, kind)
1251 text_tags = other_tags_for_text[:]
1252 if other_text_tag:
1253
1254 text_tags.append(other_text_tag)
1255 else:
1256 if gajim.config.get('chat_merge_consecutive_nickname'):
1257 if kind != old_kind or self.just_cleared:
1258 self.print_name(name, kind, other_tags_for_name)
1259 else:
1260 self.print_real_text(gajim.config.get(
1261 'chat_merge_consecutive_nickname_indent'))
1262 else:
1263 self.print_name(name, kind, other_tags_for_name)
1264 if kind == 'incoming':
1265 text_tags.append('incomingtxt')
1266 elif kind == 'outgoing':
1267 text_tags.append('outgoingtxt')
1268 self.print_subject(subject)
1269 self.print_real_text(text, text_tags, name, xhtml, graphics=graphics)
1270
1271
1272 if at_the_end or kind == 'outgoing':
1273
1274
1275 if gajim.config.get('use_smooth_scrolling'):
1276 gobject.idle_add(self.smooth_scroll_to_end)
1277 else:
1278 gobject.idle_add(self.scroll_to_end)
1279
1280 self.just_cleared = False
1281 buffer_.end_user_action()
1282
1283 - def get_time_to_show(self, tim):
1284 """
1285 Get the time, with the day before if needed and return it. It DOESN'T
1286 format a fuzzy time
1287 """
1288 format = ''
1289
1290
1291
1292 diff_day = int(timegm(time.localtime())) / 86400 -\
1293 int(timegm(tim)) / 86400
1294 if diff_day == 0:
1295 day_str = ''
1296 else:
1297
1298 day_str = i18n.ngettext('Yesterday', '%i days ago', diff_day,
1299 replace_plural=diff_day)
1300 if day_str:
1301 format += day_str + ' '
1302 timestamp_str = gajim.config.get('time_stamp')
1303 timestamp_str = helpers.from_one_line(timestamp_str)
1304 format += timestamp_str
1305 tim_format = time.strftime(format, tim)
1306 if locale.getpreferredencoding() != 'KOI8-R':
1307
1308
1309 tim_format = helpers.ensure_utf8_string(tim_format)
1310 return tim_format
1311
1312 - def detect_other_text_tag(self, text, kind):
1313 if kind == 'status':
1314 return kind
1315 elif text.startswith('/me ') or text.startswith('/me\n'):
1316 return kind
1317
1318 - def print_displaymarking(self, displaymarking):
1319 bgcolor = displaymarking.getAttr('bgcolor') or '#FFF'
1320 fgcolor = displaymarking.getAttr('fgcolor') or '#000'
1321 text = displaymarking.getData()
1322 if text:
1323 buffer_ = self.tv.get_buffer()
1324 end_iter = buffer_.get_end_iter()
1325 tag = self.displaymarking_tags.setdefault(bgcolor + '/' + fgcolor,
1326 buffer_.create_tag(None, background=bgcolor, foreground=fgcolor))
1327 buffer_.insert_with_tags(end_iter, '[' + text + ']', tag)
1328 end_iter = buffer_.get_end_iter()
1329 buffer_.insert_with_tags(end_iter, ' ')
1330
1331 - def print_name(self, name, kind, other_tags_for_name):
1332 if name:
1333 buffer_ = self.tv.get_buffer()
1334 end_iter = buffer_.get_end_iter()
1335 name_tags = other_tags_for_name[:]
1336 name_tags.append(kind)
1337 before_str = gajim.config.get('before_nickname')
1338 before_str = helpers.from_one_line(before_str)
1339 after_str = gajim.config.get('after_nickname')
1340 after_str = helpers.from_one_line(after_str)
1341 format = before_str + name + after_str + ' '
1342 buffer_.insert_with_tags_by_name(end_iter, format, *name_tags)
1343
1344 - def print_subject(self, subject):
1345 if subject:
1346 subject = _('Subject: %s\n') % subject
1347 buffer_ = self.tv.get_buffer()
1348 end_iter = buffer_.get_end_iter()
1349 buffer_.insert(end_iter, subject)
1350 self.print_empty_line()
1351
1352 - def print_real_text(self, text, text_tags=[], name=None, xhtml=None,
1353 graphics=True):
1354 """
1355 Add normal and special text. call this to add text
1356 """
1357 if xhtml:
1358 try:
1359 if name and (text.startswith('/me ') or text.startswith('/me\n')):
1360 xhtml = xhtml.replace('/me', '<i>* %s</i>' % (name,), 1)
1361 self.tv.display_html(xhtml.encode('utf-8'), self)
1362 return
1363 except Exception, e:
1364 gajim.log.debug('Error processing xhtml' + str(e))
1365 gajim.log.debug('with |' + xhtml + '|')
1366
1367
1368 if name and (text.startswith('/me ') or text.startswith('/me\n')):
1369 text = '* ' + name + text[3:]
1370 text_tags.append('italic')
1371
1372 self.detect_and_print_special_text(text, text_tags, graphics=graphics)
1373