don't open twice the same subscription request dialog. see #6762
[gajim.git] / src / chat_control.py
blobead468ba90b7a9ba04a48f5556a7f11c9d8ff0f8
1 # -*- coding:utf-8 -*-
2 ## src/chat_control.py
3 ##
4 ## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
5 ## Copyright (C) 2006-2010 Yann Leboulanger <asterix AT lagaule.org>
6 ## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
7 ## Nikos Kouremenos <kourem AT gmail.com>
8 ## Travis Shirk <travis AT pobox.com>
9 ## Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
10 ## Julien Pivotto <roidelapluie AT gmail.com>
11 ## Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
12 ## Stephan Erb <steve-e AT h3c.de>
13 ## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
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 import os
31 import time
32 import gtk
33 import pango
34 import gobject
35 import gtkgui_helpers
36 import gui_menu_builder
37 import message_control
38 import dialogs
39 import history_window
40 import notify
41 import re
43 from common import gajim
44 from common import helpers
45 from common import exceptions
46 from common import ged
47 from message_control import MessageControl
48 from conversation_textview import ConversationTextview
49 from message_textview import MessageTextView
50 from common.stanza_session import EncryptedStanzaSession, ArchivingStanzaSession
51 from common.contacts import GC_Contact
52 from common.logger import constants
53 from common.pep import MOODS, ACTIVITIES
54 from common.xmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC
55 from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION
56 from common.xmpp.protocol import NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_JINGLE_ICE_UDP
57 from common.connection_handlers_events import MessageOutgoingEvent
59 from command_system.implementation.middleware import ChatCommandProcessor
60 from command_system.implementation.middleware import CommandTools
61 from command_system.implementation.hosts import ChatCommands
63 # Here we load the module with the standard commands, so they are being detected
64 # and dispatched.
65 import command_system.implementation.standard
67 try:
68 import gtkspell
69 HAS_GTK_SPELL = True
70 except ImportError:
71 HAS_GTK_SPELL = False
73 from common import dbus_support
74 if dbus_support.supported:
75 import dbus
76 import remote_control
78 # the next script, executed in the "po" directory,
79 # generates the following list.
80 ##!/bin/sh
81 #LANG=$(for i in *.po; do j=${i/.po/}; echo -n "_('"$j"')":" '"$j"', " ; done)
82 #echo "{_('en'):'en'",$LANG"}"
83 langs = {_('English'): 'en', _('Belarusian'): 'be', _('Bulgarian'): 'bg', _('Breton'): 'br', _('Czech'): 'cs', _('German'): 'de', _('Greek'): 'el', _('British'): 'en_GB', _('Esperanto'): 'eo', _('Spanish'): 'es', _('Basque'): 'eu', _('French'): 'fr', _('Croatian'): 'hr', _('Italian'): 'it', _('Norwegian (b)'): 'nb', _('Dutch'): 'nl', _('Norwegian'): 'no', _('Polish'): 'pl', _('Portuguese'): 'pt', _('Brazilian Portuguese'): 'pt_BR', _('Russian'): 'ru', _('Serbian'): 'sr', _('Slovak'): 'sk', _('Swedish'): 'sv', _('Chinese (Ch)'): 'zh_CN'}
85 if gajim.config.get('use_speller') and HAS_GTK_SPELL:
86 # loop removing non-existent dictionaries
87 # iterating on a copy
88 tv = gtk.TextView()
89 spell = gtkspell.Spell(tv)
90 for lang in dict(langs):
91 try:
92 spell.set_language(langs[lang])
93 except OSError:
94 del langs[lang]
95 if spell:
96 spell.detach()
97 del tv
100 ################################################################################
101 class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
103 A base class containing a banner, ConversationTextview, MessageTextView
106 keymap = gtk.gdk.keymap_get_default()
107 try:
108 keycode_c = keymap.get_entries_for_keyval(gtk.keysyms.c)[0][0]
109 except TypeError:
110 keycode_c = 54
111 try:
112 keycode_ins = keymap.get_entries_for_keyval(gtk.keysyms.Insert)[0][0]
113 except TypeError:
114 keycode_ins = 118
116 def make_href(self, match):
117 url_color = gajim.config.get('urlmsgcolor')
118 url = match.group()
119 if not '://' in url:
120 url = 'http://' + url
121 return '<a href="%s"><span color="%s">%s</span></a>' % (url,
122 url_color, match.group())
124 def get_font_attrs(self):
126 Get pango font attributes for banner from theme settings
128 theme = gajim.config.get('roster_theme')
129 bannerfont = gajim.config.get_per('themes', theme, 'bannerfont')
130 bannerfontattrs = gajim.config.get_per('themes', theme, 'bannerfontattrs')
132 if bannerfont:
133 font = pango.FontDescription(bannerfont)
134 else:
135 font = pango.FontDescription('Normal')
136 if bannerfontattrs:
137 # B attribute is set by default
138 if 'B' in bannerfontattrs:
139 font.set_weight(pango.WEIGHT_HEAVY)
140 if 'I' in bannerfontattrs:
141 font.set_style(pango.STYLE_ITALIC)
143 font_attrs = 'font_desc="%s"' % font.to_string()
145 # in case there is no font specified we use x-large font size
146 if font.get_size() == 0:
147 font_attrs = '%s size="x-large"' % font_attrs
148 font.set_weight(pango.WEIGHT_NORMAL)
149 font_attrs_small = 'font_desc="%s" size="small"' % font.to_string()
150 return (font_attrs, font_attrs_small)
152 def get_nb_unread(self):
153 jid = self.contact.jid
154 if self.resource:
155 jid += '/' + self.resource
156 type_ = self.type_id
157 return len(gajim.events.get_events(self.account, jid, ['printed_' + type_,
158 type_]))
160 def draw_banner(self):
162 Draw the fat line at the top of the window that houses the icon, jid, etc
164 Derived types MAY implement this.
166 self.draw_banner_text()
167 self._update_banner_state_image()
168 gajim.plugin_manager.gui_extension_point('chat_control_base_draw_banner',
169 self)
171 def update_toolbar(self):
173 update state of buttons in toolbar
175 self._update_toolbar()
176 gajim.plugin_manager.gui_extension_point(
177 'chat_control_base_update_toolbar', self)
179 def draw_banner_text(self):
181 Derived types SHOULD implement this
183 pass
185 def update_ui(self):
187 Derived types SHOULD implement this
189 self.draw_banner()
191 def repaint_themed_widgets(self):
193 Derived types MAY implement this
195 self._paint_banner()
196 self.draw_banner()
198 def _update_banner_state_image(self):
200 Derived types MAY implement this
202 pass
204 def _update_toolbar(self):
206 Derived types MAY implement this
208 pass
210 def _nec_our_status(self, obj):
211 if self.account != obj.conn.name:
212 return
213 if obj.show == 'offline' or (obj.show == 'invisible' and \
214 obj.conn.is_zeroconf):
215 self.got_disconnected()
216 else:
217 # Other code rejoins all GCs, so we don't do it here
218 if not self.type_id == message_control.TYPE_GC:
219 self.got_connected()
220 if self.parent_win:
221 self.parent_win.redraw_tab(self)
223 def _nec_ping_sent(self, obj):
224 if self.contact != obj.contact:
225 return
226 self.print_conversation(_('Ping?'), 'status')
228 def _nec_ping_reply(self, obj):
229 if self.contact != obj.contact:
230 return
231 self.print_conversation(_('Pong! (%s s.)') % obj.seconds, 'status')
233 def _nec_ping_error(self, obj):
234 if self.contact != obj.contact:
235 return
236 self.print_conversation(_('Error.'), 'status')
238 def handle_message_textview_mykey_press(self, widget, event_keyval,
239 event_keymod):
241 Derives types SHOULD implement this, rather than connection to the even
242 itself
244 event = gtk.gdk.Event(gtk.gdk.KEY_PRESS)
245 event.keyval = event_keyval
246 event.state = event_keymod
247 event.time = 0
249 _buffer = widget.get_buffer()
250 start, end = _buffer.get_bounds()
252 if event.keyval -- gtk.keysyms.Tab:
253 position = _buffer.get_insert()
254 end = _buffer.get_iter_at_mark(position)
256 text = _buffer.get_text(start, end, False)
257 text = text.decode('utf8')
259 splitted = text.split()
261 if (text.startswith(self.COMMAND_PREFIX) and not
262 text.startswith(self.COMMAND_PREFIX * 2) and len(splitted) == 1):
264 text = splitted[0]
265 bare = text.lstrip(self.COMMAND_PREFIX)
267 if len(text) == 1:
268 self.command_hits = []
269 for command in self.list_commands():
270 for name in command.names:
271 self.command_hits.append(name)
272 else:
273 if (self.last_key_tabs and self.command_hits and
274 self.command_hits[0].startswith(bare)):
275 self.command_hits.append(self.command_hits.pop(0))
276 else:
277 self.command_hits = []
278 for command in self.list_commands():
279 for name in command.names:
280 if name.startswith(bare):
281 self.command_hits.append(name)
283 if self.command_hits:
284 _buffer.delete(start, end)
285 _buffer.insert_at_cursor(self.COMMAND_PREFIX + self.command_hits[0] + ' ')
286 self.last_key_tabs = True
288 return True
290 self.last_key_tabs = False
292 def status_url_clicked(self, widget, url):
293 helpers.launch_browser_mailer('url', url)
295 def setup_seclabel(self, combo):
296 self.seclabel_combo = combo
297 self.seclabel_combo.hide()
298 self.seclabel_combo.set_no_show_all(True)
299 lb = gtk.ListStore(str)
300 self.seclabel_combo.set_model(lb)
301 cell = gtk.CellRendererText()
302 cell.set_property('xpad', 5) # padding for status text
303 self.seclabel_combo.pack_start(cell, True)
304 # text to show is in in first column of liststore
305 self.seclabel_combo.add_attribute(cell, 'text', 0)
306 if gajim.connections[self.account].seclabel_supported:
307 gajim.connections[self.account].seclabel_catalogue(self.contact.jid, self.on_seclabels_ready)
309 def on_seclabels_ready(self):
310 lb = self.seclabel_combo.get_model()
311 lb.clear()
312 for label in gajim.connections[self.account].seclabel_catalogues[self.contact.jid][2]:
313 lb.append([label])
314 self.seclabel_combo.set_active(0)
315 self.seclabel_combo.set_no_show_all(False)
316 self.seclabel_combo.show_all()
318 def __init__(self, type_id, parent_win, widget_name, contact, acct,
319 resource=None):
320 # Undo needs this variable to know if space has been pressed.
321 # Initialize it to True so empty textview is saved in undo list
322 self.space_pressed = True
324 if resource is None:
325 # We very likely got a contact with a random resource.
326 # This is bad, we need the highest for caps etc.
327 c = gajim.contacts.get_contact_with_highest_priority(
328 acct, contact.jid)
329 if c and not isinstance(c, GC_Contact):
330 contact = c
332 MessageControl.__init__(self, type_id, parent_win, widget_name,
333 contact, acct, resource=resource)
335 widget = self.xml.get_object('history_button')
336 id_ = widget.connect('clicked', self._on_history_menuitem_activate)
337 self.handlers[id_] = widget
339 # when/if we do XHTML we will put formatting buttons back
340 widget = self.xml.get_object('emoticons_button')
341 id_ = widget.connect('clicked', self.on_emoticons_button_clicked)
342 self.handlers[id_] = widget
344 # Create banner and connect signals
345 widget = self.xml.get_object('banner_eventbox')
346 id_ = widget.connect('button-press-event',
347 self._on_banner_eventbox_button_press_event)
348 self.handlers[id_] = widget
350 self.urlfinder = re.compile(
351 r"(www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'\"]+[^!,\.\s<>\)'\"\]]")
353 self.banner_status_label = self.xml.get_object('banner_label')
354 id_ = self.banner_status_label.connect('populate_popup',
355 self.on_banner_label_populate_popup)
356 self.handlers[id_] = self.banner_status_label
358 # Init DND
359 self.TARGET_TYPE_URI_LIST = 80
360 self.dnd_list = [('text/uri-list', 0, self.TARGET_TYPE_URI_LIST),
361 ('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_APP, 0)]
362 id_ = self.widget.connect('drag_data_received',
363 self._on_drag_data_received)
364 self.handlers[id_] = self.widget
365 self.widget.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
366 gtk.DEST_DEFAULT_HIGHLIGHT |
367 gtk.DEST_DEFAULT_DROP,
368 self.dnd_list, gtk.gdk.ACTION_COPY)
370 # Create textviews and connect signals
371 self.conv_textview = ConversationTextview(self.account)
372 id_ = self.conv_textview.connect('quote', self.on_quote)
373 self.handlers[id_] = self.conv_textview.tv
374 id_ = self.conv_textview.tv.connect('key_press_event',
375 self._conv_textview_key_press_event)
376 self.handlers[id_] = self.conv_textview.tv
377 # FIXME: DND on non editable TextView, find a better way
378 self.drag_entered = False
379 id_ = self.conv_textview.tv.connect('drag_data_received',
380 self._on_drag_data_received)
381 self.handlers[id_] = self.conv_textview.tv
382 id_ = self.conv_textview.tv.connect('drag_motion', self._on_drag_motion)
383 self.handlers[id_] = self.conv_textview.tv
384 id_ = self.conv_textview.tv.connect('drag_leave', self._on_drag_leave)
385 self.handlers[id_] = self.conv_textview.tv
386 self.conv_textview.tv.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
387 gtk.DEST_DEFAULT_HIGHLIGHT |
388 gtk.DEST_DEFAULT_DROP,
389 self.dnd_list, gtk.gdk.ACTION_COPY)
391 self.conv_scrolledwindow = self.xml.get_object(
392 'conversation_scrolledwindow')
393 self.conv_scrolledwindow.add(self.conv_textview.tv)
394 widget = self.conv_scrolledwindow.get_vadjustment()
395 id_ = widget.connect('value-changed',
396 self.on_conversation_vadjustment_value_changed)
397 self.handlers[id_] = widget
398 id_ = widget.connect('changed',
399 self.on_conversation_vadjustment_changed)
400 self.handlers[id_] = widget
401 self.scroll_to_end_id = None
402 self.was_at_the_end = True
404 # add MessageTextView to UI and connect signals
405 self.msg_scrolledwindow = self.xml.get_object('message_scrolledwindow')
406 self.msg_textview = MessageTextView()
407 id_ = self.msg_textview.connect('mykeypress',
408 self._on_message_textview_mykeypress_event)
409 self.handlers[id_] = self.msg_textview
410 self.msg_scrolledwindow.add(self.msg_textview)
411 id_ = self.msg_textview.connect('key_press_event',
412 self._on_message_textview_key_press_event)
413 self.handlers[id_] = self.msg_textview
414 id_ = self.msg_textview.connect('size-request', self.size_request)
415 self.handlers[id_] = self.msg_textview
416 id_ = self.msg_textview.connect('populate_popup',
417 self.on_msg_textview_populate_popup)
418 self.handlers[id_] = self.msg_textview
419 # Setup DND
420 id_ = self.msg_textview.connect('drag_data_received',
421 self._on_drag_data_received)
422 self.handlers[id_] = self.msg_textview
423 self.msg_textview.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
424 gtk.DEST_DEFAULT_HIGHLIGHT,
425 self.dnd_list, gtk.gdk.ACTION_COPY)
427 self.update_font()
429 # Hook up send button
430 widget = self.xml.get_object('send_button')
431 id_ = widget.connect('clicked', self._on_send_button_clicked)
432 self.handlers[id_] = widget
434 widget = self.xml.get_object('formattings_button')
435 id_ = widget.connect('clicked', self.on_formattings_button_clicked)
436 self.handlers[id_] = widget
438 # the following vars are used to keep history of user's messages
439 self.sent_history = []
440 self.sent_history_pos = 0
441 self.received_history = []
442 self.received_history_pos = 0
443 self.orig_msg = None
445 # Emoticons menu
446 # set image no matter if user wants at this time emoticons or not
447 # (so toggle works ok)
448 img = self.xml.get_object('emoticons_button_image')
449 img.set_from_file(os.path.join(gajim.DATA_DIR, 'emoticons', 'static',
450 'smile.png'))
451 self.toggle_emoticons()
453 # Attach speller
454 if gajim.config.get('use_speller') and HAS_GTK_SPELL:
455 self.set_speller()
456 self.conv_textview.tv.show()
457 self._paint_banner()
459 # For XEP-0172
460 self.user_nick = None
462 self.smooth = True
463 self.msg_textview.grab_focus()
465 self.command_hits = []
466 self.last_key_tabs = False
468 # PluginSystem: adding GUI extension point for ChatControlBase
469 # instance object (also subclasses, eg. ChatControl or GroupchatControl)
470 gajim.plugin_manager.gui_extension_point('chat_control_base', self)
472 gajim.ged.register_event_handler('our-show', ged.GUI1,
473 self._nec_our_status)
474 gajim.ged.register_event_handler('ping-sent', ged.GUI1,
475 self._nec_ping_sent)
476 gajim.ged.register_event_handler('ping-reply', ged.GUI1,
477 self._nec_ping_reply)
478 gajim.ged.register_event_handler('ping-error', ged.GUI1,
479 self._nec_ping_error)
481 # This is bascially a very nasty hack to surpass the inability
482 # to properly use the super, because of the old code.
483 CommandTools.__init__(self)
485 def set_speller(self):
486 # now set the one the user selected
487 per_type = 'contacts'
488 if self.type_id == message_control.TYPE_GC:
489 per_type = 'rooms'
490 lang = gajim.config.get_per(per_type, self.contact.jid,
491 'speller_language')
492 if not lang:
493 # use the default one
494 lang = gajim.config.get('speller_language')
495 if not lang:
496 lang = gajim.LANG
497 if lang:
498 try:
499 gtkspell.Spell(self.msg_textview, lang)
500 self.msg_textview.lang = lang
501 except (gobject.GError, RuntimeError, TypeError, OSError):
502 dialogs.AspellDictError(lang)
504 def on_banner_label_populate_popup(self, label, menu):
506 Override the default context menu and add our own menutiems
508 item = gtk.SeparatorMenuItem()
509 menu.prepend(item)
511 menu2 = self.prepare_context_menu()
512 i = 0
513 for item in menu2:
514 menu2.remove(item)
515 menu.prepend(item)
516 menu.reorder_child(item, i)
517 i += 1
518 menu.show_all()
520 def shutdown(self):
521 super(ChatControlBase, self).shutdown()
522 # PluginSystem: removing GUI extension points connected with ChatControlBase
523 # instance object
524 gajim.plugin_manager.remove_gui_extension_point('chat_control_base', self)
525 gajim.plugin_manager.remove_gui_extension_point('chat_control_base_draw_banner', self)
526 gajim.ged.remove_event_handler('our-show', ged.GUI1,
527 self._nec_our_status)
529 def on_msg_textview_populate_popup(self, textview, menu):
531 Override the default context menu and we prepend an option to switch
532 languages
534 def _on_select_dictionary(widget, lang):
535 per_type = 'contacts'
536 if self.type_id == message_control.TYPE_GC:
537 per_type = 'rooms'
538 if not gajim.config.get_per(per_type, self.contact.jid):
539 gajim.config.add_per(per_type, self.contact.jid)
540 gajim.config.set_per(per_type, self.contact.jid, 'speller_language',
541 lang)
542 spell = gtkspell.get_from_text_view(self.msg_textview)
543 self.msg_textview.lang = lang
544 spell.set_language(lang)
545 widget.set_active(True)
547 item = gtk.ImageMenuItem(gtk.STOCK_UNDO)
548 menu.prepend(item)
549 id_ = item.connect('activate', self.msg_textview.undo)
550 self.handlers[id_] = item
552 item = gtk.SeparatorMenuItem()
553 menu.prepend(item)
555 item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
556 menu.prepend(item)
557 id_ = item.connect('activate', self.msg_textview.clear)
558 self.handlers[id_] = item
560 if gajim.config.get('use_speller') and HAS_GTK_SPELL:
561 item = gtk.MenuItem(_('Spelling language'))
562 menu.prepend(item)
563 submenu = gtk.Menu()
564 item.set_submenu(submenu)
565 for lang in sorted(langs):
566 item = gtk.CheckMenuItem(lang)
567 if langs[lang] == self.msg_textview.lang:
568 item.set_active(True)
569 submenu.append(item)
570 id_ = item.connect('activate', _on_select_dictionary, langs[lang])
571 self.handlers[id_] = item
573 menu.show_all()
575 def on_quote(self, widget, text):
576 text = '>' + text.replace('\n', '\n>') + '\n'
577 message_buffer = self.msg_textview.get_buffer()
578 message_buffer.insert_at_cursor(text)
580 # moved from ChatControl
581 def _on_banner_eventbox_button_press_event(self, widget, event):
583 If right-clicked, show popup
585 if event.button == 3: # right click
586 self.parent_win.popup_menu(event)
588 def _on_send_button_clicked(self, widget):
590 When send button is pressed: send the current message
592 if gajim.connections[self.account].connected < 2: # we are not connected
593 dialogs.ErrorDialog(_('A connection is not available'),
594 _('Your message can not be sent until you are connected.'))
595 return
596 message_buffer = self.msg_textview.get_buffer()
597 start_iter = message_buffer.get_start_iter()
598 end_iter = message_buffer.get_end_iter()
599 message = message_buffer.get_text(start_iter, end_iter, 0).decode('utf-8')
600 xhtml = self.msg_textview.get_xhtml()
602 # send the message
603 self.send_message(message, xhtml=xhtml)
605 def _paint_banner(self):
607 Repaint banner with theme color
609 theme = gajim.config.get('roster_theme')
610 bgcolor = gajim.config.get_per('themes', theme, 'bannerbgcolor')
611 textcolor = gajim.config.get_per('themes', theme, 'bannertextcolor')
612 # the backgrounds are colored by using an eventbox by
613 # setting the bg color of the eventbox and the fg of the name_label
614 banner_eventbox = self.xml.get_object('banner_eventbox')
615 banner_name_label = self.xml.get_object('banner_name_label')
616 self.disconnect_style_event(banner_name_label)
617 self.disconnect_style_event(self.banner_status_label)
618 if bgcolor:
619 banner_eventbox.modify_bg(gtk.STATE_NORMAL,
620 gtk.gdk.color_parse(bgcolor))
621 default_bg = False
622 else:
623 default_bg = True
624 if textcolor:
625 banner_name_label.modify_fg(gtk.STATE_NORMAL,
626 gtk.gdk.color_parse(textcolor))
627 self.banner_status_label.modify_fg(gtk.STATE_NORMAL,
628 gtk.gdk.color_parse(textcolor))
629 default_fg = False
630 else:
631 default_fg = True
632 if default_bg or default_fg:
633 self._on_style_set_event(banner_name_label, None, default_fg,
634 default_bg)
635 if self.banner_status_label.flags() & gtk.REALIZED:
636 # Widget is realized
637 self._on_style_set_event(self.banner_status_label, None, default_fg,
638 default_bg)
640 def disconnect_style_event(self, widget):
641 # Try to find the event_id
642 for id_ in self.handlers.keys():
643 if self.handlers[id_] == widget:
644 widget.disconnect(id_)
645 del self.handlers[id_]
646 break
648 def connect_style_event(self, widget, set_fg=False, set_bg=False):
649 self.disconnect_style_event(widget)
650 id_ = widget.connect('style-set', self._on_style_set_event, set_fg,
651 set_bg)
652 self.handlers[id_] = widget
654 def _on_style_set_event(self, widget, style, *opts):
656 Set style of widget from style class *.Frame.Eventbox
657 opts[0] == True -> set fg color
658 opts[1] == True -> set bg color
660 banner_eventbox = self.xml.get_object('banner_eventbox')
661 self.disconnect_style_event(widget)
662 if opts[1]:
663 bg_color = widget.style.bg[gtk.STATE_SELECTED]
664 banner_eventbox.modify_bg(gtk.STATE_NORMAL, bg_color)
665 if opts[0]:
666 fg_color = widget.style.fg[gtk.STATE_SELECTED]
667 widget.modify_fg(gtk.STATE_NORMAL, fg_color)
668 self.connect_style_event(widget, opts[0], opts[1])
670 def _conv_textview_key_press_event(self, widget, event):
671 # translate any layout to latin_layout
672 keymap = gtk.gdk.keymap_get_default()
673 keycode = keymap.get_entries_for_keyval(event.keyval)[0][0]
674 if (event.state & gtk.gdk.CONTROL_MASK and keycode in (self.keycode_c,
675 self.keycode_ins)) or (event.state & gtk.gdk.SHIFT_MASK and \
676 event.keyval in (gtk.keysyms.Page_Down, gtk.keysyms.Page_Up)):
677 return False
678 self.parent_win.notebook.emit('key_press_event', event)
679 return True
681 def show_emoticons_menu(self):
682 if not gajim.config.get('emoticons_theme'):
683 return
685 def set_emoticons_menu_position(w, msg_tv=self.msg_textview):
686 window = msg_tv.get_window(gtk.TEXT_WINDOW_WIDGET)
687 # get the window position
688 origin = window.get_origin()
689 size = window.get_size()
690 buf = msg_tv.get_buffer()
691 # get the cursor position
692 cursor = msg_tv.get_iter_location(buf.get_iter_at_mark(
693 buf.get_insert()))
694 cursor = msg_tv.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
695 cursor.x, cursor.y)
696 x = origin[0] + cursor[0]
697 y = origin[1] + size[1]
698 menu_height = gajim.interface.emoticons_menu.size_request()[1]
699 #FIXME: get_line_count is not so good
700 #get the iter of cursor, then tv.get_line_yrange
701 # so we know in which y we are typing (not how many lines we have
702 # then go show just above the current cursor line for up
703 # or just below the current cursor line for down
704 #TEST with having 3 lines and writing in the 2nd
705 if y + menu_height > gtk.gdk.screen_height():
706 # move menu just above cursor
707 y -= menu_height + (msg_tv.allocation.height / buf.get_line_count())
708 #else: # move menu just below cursor
709 # y -= (msg_tv.allocation.height / buf.get_line_count())
710 return (x, y, True) # push_in True
711 gajim.interface.emoticon_menuitem_clicked = self.append_emoticon
712 gajim.interface.emoticons_menu.popup(None, None,
713 set_emoticons_menu_position, 1, 0)
715 def _on_message_textview_key_press_event(self, widget, event):
716 if event.keyval == gtk.keysyms.space:
717 self.space_pressed = True
719 elif (self.space_pressed or self.msg_textview.undo_pressed) and \
720 event.keyval not in (gtk.keysyms.Control_L, gtk.keysyms.Control_R) and \
721 not (event.keyval == gtk.keysyms.z and event.state & gtk.gdk.CONTROL_MASK):
722 # If the space key has been pressed and now it hasnt,
723 # we save the buffer into the undo list. But be carefull we're not
724 # pressiong Control again (as in ctrl+z)
725 _buffer = widget.get_buffer()
726 start_iter, end_iter = _buffer.get_bounds()
727 self.msg_textview.save_undo(_buffer.get_text(start_iter, end_iter))
728 self.space_pressed = False
730 # Ctrl [+ Shift] + Tab are not forwarded to notebook. We handle it here
731 if self.widget_name == 'groupchat_control':
732 if event.keyval not in (gtk.keysyms.ISO_Left_Tab, gtk.keysyms.Tab):
733 self.last_key_tabs = False
734 if event.state & gtk.gdk.SHIFT_MASK:
735 # CTRL + SHIFT + TAB
736 if event.state & gtk.gdk.CONTROL_MASK and \
737 event.keyval == gtk.keysyms.ISO_Left_Tab:
738 self.parent_win.move_to_next_unread_tab(False)
739 return True
740 # SHIFT + PAGE_[UP|DOWN]: send to conv_textview
741 elif event.keyval == gtk.keysyms.Page_Down or \
742 event.keyval == gtk.keysyms.Page_Up:
743 self.conv_textview.tv.emit('key_press_event', event)
744 return True
745 elif event.state & gtk.gdk.CONTROL_MASK:
746 if event.keyval == gtk.keysyms.Tab: # CTRL + TAB
747 self.parent_win.move_to_next_unread_tab(True)
748 return True
749 return False
751 def _on_message_textview_mykeypress_event(self, widget, event_keyval,
752 event_keymod):
754 When a key is pressed: if enter is pressed without the shift key, message
755 (if not empty) is sent and printed in the conversation
757 # NOTE: handles mykeypress which is custom signal connected to this
758 # CB in new_tab(). for this singal see message_textview.py
759 message_textview = widget
760 message_buffer = message_textview.get_buffer()
761 start_iter, end_iter = message_buffer.get_bounds()
762 message = message_buffer.get_text(start_iter, end_iter, False).decode(
763 'utf-8')
764 xhtml = self.msg_textview.get_xhtml()
766 # construct event instance from binding
767 event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) # it's always a key-press here
768 event.keyval = event_keyval
769 event.state = event_keymod
770 event.time = 0 # assign current time
772 if event.keyval == gtk.keysyms.Up:
773 if event.state == gtk.gdk.CONTROL_MASK: # Ctrl+UP
774 self.scroll_messages('up', message_buffer, 'sent')
775 # Ctrl+Shift+UP
776 elif event.state == (gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK):
777 self.scroll_messages('up', message_buffer, 'received')
778 elif event.keyval == gtk.keysyms.Down:
779 if event.state == gtk.gdk.CONTROL_MASK: # Ctrl+Down
780 self.scroll_messages('down', message_buffer, 'sent')
781 # Ctrl+Shift+Down
782 elif event.state == (gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK):
783 self.scroll_messages('down', message_buffer, 'received')
784 elif event.keyval == gtk.keysyms.Return or \
785 event.keyval == gtk.keysyms.KP_Enter: # ENTER
786 # NOTE: SHIFT + ENTER is not needed to be emulated as it is not
787 # binding at all (textview's default action is newline)
789 if gajim.config.get('send_on_ctrl_enter'):
790 # here, we emulate GTK default action on ENTER (add new line)
791 # normally I would add in keypress but it gets way to complex
792 # to get instant result on changing this advanced setting
793 if event.state == 0: # no ctrl, no shift just ENTER add newline
794 end_iter = message_buffer.get_end_iter()
795 message_buffer.insert_at_cursor('\n')
796 send_message = False
797 elif event.state & gtk.gdk.CONTROL_MASK: # CTRL + ENTER
798 send_message = True
799 else: # send on Enter, do newline on Ctrl Enter
800 if event.state & gtk.gdk.CONTROL_MASK: # Ctrl + ENTER
801 end_iter = message_buffer.get_end_iter()
802 message_buffer.insert_at_cursor('\n')
803 send_message = False
804 else: # ENTER
805 send_message = True
807 if gajim.connections[self.account].connected < 2 and send_message:
808 # we are not connected
809 dialogs.ErrorDialog(_('A connection is not available'),
810 _('Your message can not be sent until you are connected.'))
811 send_message = False
813 if send_message:
814 self.send_message(message, xhtml=xhtml) # send the message
815 elif event.keyval == gtk.keysyms.z: # CTRL+z
816 if event.state & gtk.gdk.CONTROL_MASK:
817 self.msg_textview.undo()
818 else:
819 # Give the control itself a chance to process
820 self.handle_message_textview_mykey_press(widget, event_keyval,
821 event_keymod)
823 def _on_drag_data_received(self, widget, context, x, y, selection,
824 target_type, timestamp):
826 Derived types SHOULD implement this
828 pass
830 def _on_drag_leave(self, widget, context, time):
831 # FIXME: DND on non editable TextView, find a better way
832 self.drag_entered = False
833 self.conv_textview.tv.set_editable(False)
835 def _on_drag_motion(self, widget, context, x, y, time):
836 # FIXME: DND on non editable TextView, find a better way
837 if not self.drag_entered:
838 # We drag new data over the TextView, make it editable to catch dnd
839 self.drag_entered_conv = True
840 self.conv_textview.tv.set_editable(True)
842 def get_seclabel(self):
843 label = None
844 if self.seclabel_combo is not None:
845 idx = self.seclabel_combo.get_active()
846 if idx != -1:
847 cat = gajim.connections[self.account].seclabel_catalogues[self.contact.jid]
848 lname = cat[2][idx]
849 label = cat[1][lname]
850 return label
852 def send_message(self, message, keyID='', type_='chat', chatstate=None,
853 msg_id=None, composing_xep=None, resource=None, xhtml=None, callback=None,
854 callback_args=[], process_commands=True):
856 Send the given message to the active tab. Doesn't return None if error
858 if not message or message == '\n':
859 return None
861 if process_commands and self.process_as_command(message):
862 return
864 label = self.get_seclabel()
866 gajim.nec.push_outgoing_event(MessageOutgoingEvent(None,
867 account=self.account, jid=self.contact.jid, message=message,
868 keyID=keyID, type_=type_, chatstate=chatstate, msg_id=msg_id,
869 composing_xep=composing_xep, resource=resource,
870 user_nick=self.user_nick, xhtml=xhtml, label=label,
871 callback=callback, callback_args= callback_args))
873 # Record the history of sent messages
874 self.save_message(message, 'sent')
876 # Be sure to send user nickname only once according to JEP-0172
877 self.user_nick = None
879 # Clear msg input
880 message_buffer = self.msg_textview.get_buffer()
881 message_buffer.set_text('') # clear message buffer (and tv of course)
883 def save_message(self, message, msg_type):
884 # save the message, so user can scroll though the list with key up/down
885 if msg_type == 'sent':
886 history = self.sent_history
887 pos = self.sent_history_pos
888 else:
889 history = self.received_history
890 pos = self.received_history_pos
891 size = len(history)
892 scroll = False if pos == size else True # are we scrolling?
893 # we don't want size of the buffer to grow indefinately
894 max_size = gajim.config.get('key_up_lines')
895 for i in xrange(size - max_size + 1):
896 if pos == 0:
897 break
898 history.pop(0)
899 pos -= 1
900 history.append(message)
901 if not scroll or msg_type == 'sent':
902 pos = len(history)
903 if msg_type == 'sent':
904 self.sent_history_pos = pos
905 self.orig_msg = None
906 else:
907 self.received_history_pos = pos
909 def print_conversation_line(self, text, kind, name, tim,
910 other_tags_for_name=[], other_tags_for_time=[],
911 other_tags_for_text=[], count_as_new=True, subject=None,
912 old_kind=None, xhtml=None, simple=False, xep0184_id=None,
913 graphics=True, displaymarking=None):
915 Print 'chat' type messages
917 jid = self.contact.jid
918 full_jid = self.get_full_jid()
919 textview = self.conv_textview
920 end = False
921 if self.was_at_the_end or kind == 'outgoing':
922 end = True
923 textview.print_conversation_line(text, jid, kind, name, tim,
924 other_tags_for_name, other_tags_for_time, other_tags_for_text,
925 subject, old_kind, xhtml, simple=simple, graphics=graphics,
926 displaymarking=displaymarking)
928 if xep0184_id is not None:
929 textview.show_xep0184_warning(xep0184_id)
931 if not count_as_new:
932 return
933 if kind == 'incoming':
934 if not self.type_id == message_control.TYPE_GC or \
935 gajim.config.get('notify_on_all_muc_messages') or \
936 'marked' in other_tags_for_text:
937 # it's a normal message, or a muc message with want to be
938 # notified about if quitting just after
939 # other_tags_for_text == ['marked'] --> highlighted gc message
940 gajim.last_message_time[self.account][full_jid] = time.time()
942 if kind in ('incoming', 'incoming_queue'):
943 # Record the history of received messages
944 self.save_message(text, 'received')
946 if kind in ('incoming', 'incoming_queue', 'error'):
947 gc_message = False
948 if self.type_id == message_control.TYPE_GC:
949 gc_message = True
951 if ((self.parent_win and (not self.parent_win.get_active_control() or \
952 self != self.parent_win.get_active_control() or \
953 not self.parent_win.is_active() or not end)) or \
954 (gc_message and \
955 jid in gajim.interface.minimized_controls[self.account])) and \
956 kind in ('incoming', 'incoming_queue', 'error'):
957 # we want to have save this message in events list
958 # other_tags_for_text == ['marked'] --> highlighted gc message
959 if gc_message:
960 if 'marked' in other_tags_for_text:
961 type_ = 'printed_marked_gc_msg'
962 else:
963 type_ = 'printed_gc_msg'
964 event = 'gc_message_received'
965 else:
966 type_ = 'printed_' + self.type_id
967 event = 'message_received'
968 show_in_roster = notify.get_show_in_roster(event,
969 self.account, self.contact, self.session)
970 show_in_systray = notify.get_show_in_systray(event,
971 self.account, self.contact, type_)
973 event = gajim.events.create_event(type_, (self,),
974 show_in_roster=show_in_roster,
975 show_in_systray=show_in_systray)
976 gajim.events.add_event(self.account, full_jid, event)
977 # We need to redraw contact if we show in roster
978 if show_in_roster:
979 gajim.interface.roster.draw_contact(self.contact.jid,
980 self.account)
982 if not self.parent_win:
983 return
985 if (not self.parent_win.get_active_control() or \
986 self != self.parent_win.get_active_control() or \
987 not self.parent_win.is_active() or not end) and \
988 kind in ('incoming', 'incoming_queue', 'error'):
989 self.parent_win.redraw_tab(self)
990 if not self.parent_win.is_active():
991 self.parent_win.show_title(True, self) # Enabled Urgent hint
992 else:
993 self.parent_win.show_title(False, self) # Disabled Urgent hint
995 def toggle_emoticons(self):
997 Hide show emoticons_button and make sure emoticons_menu is always there
998 when needed
1000 emoticons_button = self.xml.get_object('emoticons_button')
1001 if gajim.config.get('emoticons_theme'):
1002 emoticons_button.show()
1003 emoticons_button.set_no_show_all(False)
1004 else:
1005 emoticons_button.hide()
1006 emoticons_button.set_no_show_all(True)
1008 def append_emoticon(self, str_):
1009 buffer_ = self.msg_textview.get_buffer()
1010 if buffer_.get_char_count():
1011 buffer_.insert_at_cursor(' %s ' % str_)
1012 else: # we are the beginning of buffer
1013 buffer_.insert_at_cursor('%s ' % str_)
1014 self.msg_textview.grab_focus()
1016 def on_emoticons_button_clicked(self, widget):
1018 Popup emoticons menu
1020 gajim.interface.emoticon_menuitem_clicked = self.append_emoticon
1021 gajim.interface.popup_emoticons_under_button(widget, self.parent_win)
1023 def on_formattings_button_clicked(self, widget):
1025 Popup formattings menu
1027 menu = gtk.Menu()
1029 menuitems = ((_('Bold'), 'bold'),
1030 (_('Italic'), 'italic'),
1031 (_('Underline'), 'underline'),
1032 (_('Strike'), 'strike'))
1034 active_tags = self.msg_textview.get_active_tags()
1036 for menuitem in menuitems:
1037 item = gtk.CheckMenuItem(menuitem[0])
1038 if menuitem[1] in active_tags:
1039 item.set_active(True)
1040 else:
1041 item.set_active(False)
1042 item.connect('activate', self.msg_textview.set_tag,
1043 menuitem[1])
1044 menu.append(item)
1046 item = gtk.SeparatorMenuItem() # separator
1047 menu.append(item)
1049 item = gtk.ImageMenuItem(_('Color'))
1050 icon = gtk.image_new_from_stock(gtk.STOCK_SELECT_COLOR, gtk.ICON_SIZE_MENU)
1051 item.set_image(icon)
1052 item.connect('activate', self.on_color_menuitem_activale)
1053 menu.append(item)
1055 item = gtk.ImageMenuItem(_('Font'))
1056 icon = gtk.image_new_from_stock(gtk.STOCK_SELECT_FONT, gtk.ICON_SIZE_MENU)
1057 item.set_image(icon)
1058 item.connect('activate', self.on_font_menuitem_activale)
1059 menu.append(item)
1061 item = gtk.SeparatorMenuItem() # separator
1062 menu.append(item)
1064 item = gtk.ImageMenuItem(_('Clear formating'))
1065 icon = gtk.image_new_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU)
1066 item.set_image(icon)
1067 item.connect('activate', self.msg_textview.clear_tags)
1068 menu.append(item)
1070 menu.show_all()
1071 gtkgui_helpers.popup_emoticons_under_button(menu, widget,
1072 self.parent_win)
1074 def on_color_menuitem_activale(self, widget):
1075 color_dialog = gtk.ColorSelectionDialog('Select a color')
1076 color_dialog.connect('response', self.msg_textview.color_set,
1077 color_dialog.colorsel)
1078 color_dialog.show_all()
1080 def on_font_menuitem_activale(self, widget):
1081 font_dialog = gtk.FontSelectionDialog('Select a font')
1082 font_dialog.connect('response', self.msg_textview.font_set,
1083 font_dialog.fontsel)
1084 font_dialog.show_all()
1086 def on_actions_button_clicked(self, widget):
1088 Popup action menu
1090 menu = self.prepare_context_menu(hide_buttonbar_items=True)
1091 menu.show_all()
1092 gtkgui_helpers.popup_emoticons_under_button(menu, widget,
1093 self.parent_win)
1095 def update_font(self):
1096 font = pango.FontDescription(gajim.config.get('conversation_font'))
1097 self.conv_textview.tv.modify_font(font)
1098 self.msg_textview.modify_font(font)
1100 def update_tags(self):
1101 self.conv_textview.update_tags()
1103 def clear(self, tv):
1104 buffer_ = tv.get_buffer()
1105 start, end = buffer_.get_bounds()
1106 buffer_.delete(start, end)
1108 def _on_history_menuitem_activate(self, widget=None, jid=None):
1110 When history menuitem is pressed: call history window
1112 if not jid:
1113 jid = self.contact.jid
1115 if 'logs' in gajim.interface.instances:
1116 gajim.interface.instances['logs'].window.present()
1117 gajim.interface.instances['logs'].open_history(jid, self.account)
1118 else:
1119 gajim.interface.instances['logs'] = \
1120 history_window.HistoryWindow(jid, self.account)
1122 def _on_send_file(self, gc_contact=None):
1124 gc_contact can be set when we are in a groupchat control
1126 def _on_ok(c):
1127 gajim.interface.instances['file_transfers'].show_file_send_request(
1128 self.account, c)
1129 if self.TYPE_ID == message_control.TYPE_PM:
1130 gc_contact = self.gc_contact
1131 if gc_contact:
1132 # gc or pm
1133 gc_control = gajim.interface.msg_win_mgr.get_gc_control(
1134 gc_contact.room_jid, self.account)
1135 self_contact = gajim.contacts.get_gc_contact(self.account,
1136 gc_control.room_jid, gc_control.nick)
1137 if gc_control.is_anonymous and gc_contact.affiliation not in ['admin',
1138 'owner'] and self_contact.affiliation in ['admin', 'owner']:
1139 contact = gajim.contacts.get_contact(self.account, gc_contact.jid)
1140 if not contact or contact.sub not in ('both', 'to'):
1141 prim_text = _('Really send file?')
1142 sec_text = _('If you send a file to %s, he/she will know your '
1143 'real Jabber ID.') % gc_contact.name
1144 dialog = dialogs.NonModalConfirmationDialog(prim_text,
1145 sec_text, on_response_ok=(_on_ok, gc_contact))
1146 dialog.popup()
1147 return
1148 _on_ok(gc_contact)
1149 return
1150 _on_ok(self.contact)
1152 def on_minimize_menuitem_toggled(self, widget):
1154 When a grouchat is minimized, unparent the tab, put it in roster etc
1156 old_value = False
1157 minimized_gc = gajim.config.get_per('accounts', self.account,
1158 'minimized_gc').split()
1159 if self.contact.jid in minimized_gc:
1160 old_value = True
1161 minimize = widget.get_active()
1162 if minimize and not self.contact.jid in minimized_gc:
1163 minimized_gc.append(self.contact.jid)
1164 if not minimize and self.contact.jid in minimized_gc:
1165 minimized_gc.remove(self.contact.jid)
1166 if old_value != minimize:
1167 gajim.config.set_per('accounts', self.account, 'minimized_gc',
1168 ' '.join(minimized_gc))
1170 def set_control_active(self, state):
1171 if state:
1172 jid = self.contact.jid
1173 if self.was_at_the_end:
1174 # we are at the end
1175 type_ = ['printed_' + self.type_id]
1176 if self.type_id == message_control.TYPE_GC:
1177 type_ = ['printed_gc_msg', 'printed_marked_gc_msg']
1178 if not gajim.events.remove_events(self.account, self.get_full_jid(),
1179 types=type_):
1180 # There were events to remove
1181 self.redraw_after_event_removed(jid)
1183 def bring_scroll_to_end(self, textview, diff_y=0):
1185 Scroll to the end of textview if end is not visible
1187 if self.scroll_to_end_id:
1188 # a scroll is already planned
1189 return
1190 buffer_ = textview.get_buffer()
1191 end_iter = buffer_.get_end_iter()
1192 end_rect = textview.get_iter_location(end_iter)
1193 visible_rect = textview.get_visible_rect()
1194 # scroll only if expected end is not visible
1195 if end_rect.y >= (visible_rect.y + visible_rect.height + diff_y):
1196 self.scroll_to_end_id = gobject.idle_add(self.scroll_to_end_iter,
1197 textview)
1199 def scroll_to_end_iter(self, textview):
1200 buffer_ = textview.get_buffer()
1201 end_iter = buffer_.get_end_iter()
1202 textview.scroll_to_iter(end_iter, 0, False, 1, 1)
1203 self.scroll_to_end_id = None
1204 return False
1206 def size_request(self, msg_textview, requisition):
1208 When message_textview changes its size: if the new height will enlarge
1209 the window, enable the scrollbar automatic policy. Also enable scrollbar
1210 automatic policy for horizontal scrollbar if message we have in
1211 message_textview is too big
1213 if msg_textview.window is None:
1214 return
1216 min_height = self.conv_scrolledwindow.get_property('height-request')
1217 conversation_height = self.conv_textview.tv.window.get_size()[1]
1218 message_height = msg_textview.window.get_size()[1]
1219 message_width = msg_textview.window.get_size()[0]
1220 # new tab is not exposed yet
1221 if conversation_height < 2:
1222 return
1224 if conversation_height < min_height:
1225 min_height = conversation_height
1227 # we don't want to always resize in height the message_textview
1228 # so we have minimum on conversation_textview's scrolled window
1229 # but we also want to avoid window resizing so if we reach that
1230 # minimum for conversation_textview and maximum for message_textview
1231 # we set to automatic the scrollbar policy
1232 diff_y = message_height - requisition.height
1233 if diff_y != 0:
1234 if conversation_height + diff_y < min_height:
1235 if message_height + conversation_height - min_height > min_height:
1236 policy = self.msg_scrolledwindow.get_property(
1237 'vscrollbar-policy')
1238 if policy != gtk.POLICY_AUTOMATIC:
1239 self.msg_scrolledwindow.set_property('vscrollbar-policy',
1240 gtk.POLICY_AUTOMATIC)
1241 self.msg_scrolledwindow.set_property('height-request',
1242 message_height + conversation_height - min_height)
1243 else:
1244 self.msg_scrolledwindow.set_property('vscrollbar-policy',
1245 gtk.POLICY_NEVER)
1246 self.msg_scrolledwindow.set_property('height-request', -1)
1248 self.smooth = True # reinit the flag
1249 # enable scrollbar automatic policy for horizontal scrollbar
1250 # if message we have in message_textview is too big
1251 if requisition.width > message_width:
1252 self.msg_scrolledwindow.set_property('hscrollbar-policy',
1253 gtk.POLICY_AUTOMATIC)
1254 else:
1255 self.msg_scrolledwindow.set_property('hscrollbar-policy',
1256 gtk.POLICY_NEVER)
1258 return True
1260 def on_conversation_vadjustment_changed(self, adjustment):
1261 # used to stay at the end of the textview when we shrink conversation
1262 # textview.
1263 if self.was_at_the_end:
1264 self.conv_textview.bring_scroll_to_end(-18)
1265 self.was_at_the_end = (adjustment.upper - adjustment.value - adjustment.page_size) < 18
1267 def on_conversation_vadjustment_value_changed(self, adjustment):
1268 # stop automatic scroll when we manually scroll
1269 if not self.conv_textview.auto_scrolling:
1270 self.conv_textview.stop_scrolling()
1271 self.was_at_the_end = (adjustment.upper - adjustment.value - adjustment.page_size) < 18
1272 if self.resource:
1273 jid = self.contact.get_full_jid()
1274 else:
1275 jid = self.contact.jid
1276 types_list = []
1277 type_ = self.type_id
1278 if type_ == message_control.TYPE_GC:
1279 type_ = 'gc_msg'
1280 types_list = ['printed_' + type_, type_, 'printed_marked_gc_msg']
1281 else: # Not a GC
1282 types_list = ['printed_' + type_, type_]
1284 if not len(gajim.events.get_events(self.account, jid, types_list)):
1285 return
1286 if not self.parent_win:
1287 return
1288 if self.conv_textview.at_the_end() and \
1289 self.parent_win.get_active_control() == self and \
1290 self.parent_win.window.is_active():
1291 # we are at the end
1292 if self.type_id == message_control.TYPE_GC:
1293 if not gajim.events.remove_events(self.account, jid,
1294 types=types_list):
1295 self.redraw_after_event_removed(jid)
1296 elif self.session and self.session.remove_events(types_list):
1297 # There were events to remove
1298 self.redraw_after_event_removed(jid)
1300 def redraw_after_event_removed(self, jid):
1302 We just removed a 'printed_*' event, redraw contact in roster or
1303 gc_roster and titles in roster and msg_win
1305 self.parent_win.redraw_tab(self)
1306 self.parent_win.show_title()
1307 # TODO : get the contact and check notify.get_show_in_roster()
1308 if self.type_id == message_control.TYPE_PM:
1309 room_jid, nick = gajim.get_room_and_nick_from_fjid(jid)
1310 groupchat_control = gajim.interface.msg_win_mgr.get_gc_control(
1311 room_jid, self.account)
1312 if room_jid in gajim.interface.minimized_controls[self.account]:
1313 groupchat_control = \
1314 gajim.interface.minimized_controls[self.account][room_jid]
1315 contact = \
1316 gajim.contacts.get_contact_with_highest_priority(self.account, \
1317 room_jid)
1318 if contact:
1319 gajim.interface.roster.draw_contact(room_jid, self.account)
1320 if groupchat_control:
1321 groupchat_control.draw_contact(nick)
1322 if groupchat_control.parent_win:
1323 groupchat_control.parent_win.redraw_tab(groupchat_control)
1324 else:
1325 gajim.interface.roster.draw_contact(jid, self.account)
1326 gajim.interface.roster.show_title()
1328 def scroll_messages(self, direction, msg_buf, msg_type):
1329 if msg_type == 'sent':
1330 history = self.sent_history
1331 pos = self.sent_history_pos
1332 self.received_history_pos = len(self.received_history)
1333 else:
1334 history = self.received_history
1335 pos = self.received_history_pos
1336 self.sent_history_pos = len(self.sent_history)
1337 size = len(history)
1338 if self.orig_msg is None:
1339 # user was typing something and then went into history, so save
1340 # whatever is already typed
1341 start_iter = msg_buf.get_start_iter()
1342 end_iter = msg_buf.get_end_iter()
1343 self.orig_msg = msg_buf.get_text(start_iter, end_iter, 0).decode(
1344 'utf-8')
1345 pos += -1 if direction == 'up' else +1
1346 if pos == -1:
1347 return
1348 if pos >= size:
1349 pos = size
1350 message = self.orig_msg
1351 self.orig_msg = None
1352 else:
1353 message = history[pos]
1354 if msg_type == 'sent':
1355 self.sent_history_pos = pos
1356 else:
1357 self.received_history_pos = pos
1358 if self.orig_msg is not None:
1359 message = '> %s\n' % message.replace('\n', '\n> ')
1360 msg_buf.set_text(message)
1362 def lighten_color(self, color):
1363 p = 0.4
1364 mask = 0
1365 color.red = int((color.red * p) + (mask * (1 - p)))
1366 color.green = int((color.green * p) + (mask * (1 - p)))
1367 color.blue = int((color.blue * p) + (mask * (1 - p)))
1368 return color
1370 def widget_set_visible(self, widget, state):
1372 Show or hide a widget
1374 # make the last message visible, when changing to "full view"
1375 if not state:
1376 gobject.idle_add(self.conv_textview.scroll_to_end_iter)
1378 widget.set_no_show_all(state)
1379 if state:
1380 widget.hide()
1381 else:
1382 widget.show_all()
1384 def chat_buttons_set_visible(self, state):
1386 Toggle chat buttons
1388 MessageControl.chat_buttons_set_visible(self, state)
1389 self.widget_set_visible(self.xml.get_object('actions_hbox'), state)
1391 def got_connected(self):
1392 self.msg_textview.set_sensitive(True)
1393 self.msg_textview.set_editable(True)
1394 # FIXME: Set sensitivity for toolbar
1396 def got_disconnected(self):
1397 self.msg_textview.set_sensitive(False)
1398 self.msg_textview.set_editable(False)
1399 self.conv_textview.tv.grab_focus()
1401 self.no_autonegotiation = False
1402 # FIXME: Set sensitivity for toolbar
1405 ################################################################################
1406 class ChatControl(ChatControlBase):
1408 A control for standard 1-1 chat
1411 JINGLE_STATE_NULL,
1412 JINGLE_STATE_CONNECTING,
1413 JINGLE_STATE_CONNECTION_RECEIVED,
1414 JINGLE_STATE_CONNECTED,
1415 JINGLE_STATE_ERROR
1416 ) = range(5)
1418 TYPE_ID = message_control.TYPE_CHAT
1419 old_msg_kind = None # last kind of the printed message
1421 # Set a command host to bound to. Every command given through a chat will be
1422 # processed with this command host.
1423 COMMAND_HOST = ChatCommands
1425 def __init__(self, parent_win, contact, acct, session, resource=None):
1426 ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
1427 'chat_control', contact, acct, resource)
1429 self._dbus_message_sent_match = None
1430 if dbus_support.supported:
1431 bus = dbus_support.session_bus.bus()
1432 try:
1433 obj = bus.get_object(remote_control.SERVICE, remote_control.OBJ_PATH)
1434 except:
1435 # likely dbus service not started
1436 pass
1437 else:
1438 iface = dbus.Interface(obj, remote_control.INTERFACE)
1439 self._dbus_message_sent_match = iface.connect_to_signal("MessageSent", self.on_message_sent)
1441 self.gpg_is_active = False
1442 # for muc use:
1443 # widget = self.xml.get_object('muc_window_actions_button')
1444 self.actions_button = self.xml.get_object('message_window_actions_button')
1445 id_ = self.actions_button.connect('clicked',
1446 self.on_actions_button_clicked)
1447 self.handlers[id_] = self.actions_button
1449 self._formattings_button = self.xml.get_object('formattings_button')
1451 self._add_to_roster_button = self.xml.get_object(
1452 'add_to_roster_button')
1453 id_ = self._add_to_roster_button.connect('clicked',
1454 self._on_add_to_roster_menuitem_activate)
1455 self.handlers[id_] = self._add_to_roster_button
1457 self._audio_button = self.xml.get_object('audio_togglebutton')
1458 id_ = self._audio_button.connect('toggled', self.on_audio_button_toggled)
1459 self.handlers[id_] = self._audio_button
1460 # add a special img
1461 gtkgui_helpers.add_image_to_button(self._audio_button,
1462 'gajim-mic_inactive')
1464 self._video_button = self.xml.get_object('video_togglebutton')
1465 id_ = self._video_button.connect('toggled', self.on_video_button_toggled)
1466 self.handlers[id_] = self._video_button
1467 # add a special img
1468 gtkgui_helpers.add_image_to_button(self._video_button,
1469 'gajim-cam_inactive')
1471 self._send_file_button = self.xml.get_object('send_file_button')
1472 # add a special img for send file button
1473 path_to_upload_img = gtkgui_helpers.get_icon_path('gajim-upload')
1474 img = gtk.Image()
1475 img.set_from_file(path_to_upload_img)
1476 self._send_file_button.set_image(img)
1477 id_ = self._send_file_button.connect('clicked',
1478 self._on_send_file_menuitem_activate)
1479 self.handlers[id_] = self._send_file_button
1481 self._convert_to_gc_button = self.xml.get_object(
1482 'convert_to_gc_button')
1483 id_ = self._convert_to_gc_button.connect('clicked',
1484 self._on_convert_to_gc_menuitem_activate)
1485 self.handlers[id_] = self._convert_to_gc_button
1487 contact_information_button = self.xml.get_object(
1488 'contact_information_button')
1489 id_ = contact_information_button.connect('clicked',
1490 self._on_contact_information_menuitem_activate)
1491 self.handlers[id_] = contact_information_button
1493 compact_view = gajim.config.get('compact_view')
1494 self.chat_buttons_set_visible(compact_view)
1495 self.widget_set_visible(self.xml.get_object('banner_eventbox'),
1496 gajim.config.get('hide_chat_banner'))
1498 self.authentication_button = self.xml.get_object(
1499 'authentication_button')
1500 id_ = self.authentication_button.connect('clicked',
1501 self._on_authentication_button_clicked)
1502 self.handlers[id_] = self.authentication_button
1504 # Add lock image to show chat encryption
1505 self.lock_image = self.xml.get_object('lock_image')
1507 # Convert to GC icon
1508 img = self.xml.get_object('convert_to_gc_button_image')
1509 img.set_from_pixbuf(gtkgui_helpers.load_icon(
1510 'muc_active').get_pixbuf())
1512 self._audio_banner_image = self.xml.get_object('audio_banner_image')
1513 self._video_banner_image = self.xml.get_object('video_banner_image')
1514 self.audio_sid = None
1515 self.audio_state = self.JINGLE_STATE_NULL
1516 self.audio_available = False
1517 self.video_sid = None
1518 self.video_state = self.JINGLE_STATE_NULL
1519 self.video_available = False
1521 self.update_toolbar()
1523 self._pep_images = {}
1524 self._pep_images['mood'] = self.xml.get_object('mood_image')
1525 self._pep_images['activity'] = self.xml.get_object('activity_image')
1526 self._pep_images['tune'] = self.xml.get_object('tune_image')
1527 self._pep_images['location'] = self.xml.get_object('location_image')
1528 self.update_all_pep_types()
1530 # keep timeout id and window obj for possible big avatar
1531 # it is on enter-notify and leave-notify so no need to be
1532 # per jid
1533 self.show_bigger_avatar_timeout_id = None
1534 self.bigger_avatar_window = None
1535 self.show_avatar()
1537 # chatstate timers and state
1538 self.reset_kbd_mouse_timeout_vars()
1539 self._schedule_activity_timers()
1541 # Hook up signals
1542 id_ = self.parent_win.window.connect('motion-notify-event',
1543 self._on_window_motion_notify)
1544 self.handlers[id_] = self.parent_win.window
1545 message_tv_buffer = self.msg_textview.get_buffer()
1546 id_ = message_tv_buffer.connect('changed',
1547 self._on_message_tv_buffer_changed)
1548 self.handlers[id_] = message_tv_buffer
1550 widget = self.xml.get_object('avatar_eventbox')
1551 widget.set_property('height-request', gajim.config.get(
1552 'chat_avatar_height'))
1553 id_ = widget.connect('enter-notify-event',
1554 self.on_avatar_eventbox_enter_notify_event)
1555 self.handlers[id_] = widget
1557 id_ = widget.connect('leave-notify-event',
1558 self.on_avatar_eventbox_leave_notify_event)
1559 self.handlers[id_] = widget
1561 id_ = widget.connect('button-press-event',
1562 self.on_avatar_eventbox_button_press_event)
1563 self.handlers[id_] = widget
1565 widget = self.xml.get_object('location_eventbox')
1566 id_ = widget.connect('button-release-event',
1567 self.on_location_eventbox_button_release_event)
1568 self.handlers[id_] = widget
1570 for key in ('1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'):
1571 widget = self.xml.get_object(key + '_button')
1572 id_ = widget.connect('pressed', self.on_num_button_pressed, key)
1573 self.handlers[id_] = widget
1574 id_ = widget.connect('released', self.on_num_button_released)
1575 self.handlers[id_] = widget
1577 self.dtmf_window = self.xml.get_object('dtmf_window')
1578 id_ = self.dtmf_window.connect('focus-out-event',
1579 self.on_dtmf_window_focus_out_event)
1580 self.handlers[id_] = self.dtmf_window
1582 widget = self.xml.get_object('dtmf_button')
1583 id_ = widget.connect('clicked', self.on_dtmf_button_clicked)
1584 self.handlers[id_] = widget
1586 widget = self.xml.get_object('mic_hscale')
1587 id_ = widget.connect('value_changed', self.on_mic_hscale_value_changed)
1588 self.handlers[id_] = widget
1590 widget = self.xml.get_object('sound_hscale')
1591 id_ = widget.connect('value_changed', self.on_sound_hscale_value_changed)
1592 self.handlers[id_] = widget
1594 if not session:
1595 # Don't use previous session if we want to a specific resource
1596 # and it's not the same
1597 if not resource:
1598 resource = contact.resource
1599 session = gajim.connections[self.account].find_controlless_session(
1600 self.contact.jid, resource)
1602 self.setup_seclabel(self.xml.get_object('label_selector'))
1603 if session:
1604 session.control = self
1605 self.session = session
1607 if session.enable_encryption:
1608 self.print_esession_details()
1610 # Enable encryption if needed
1611 self.no_autonegotiation = False
1612 e2e_is_active = self.session and self.session.enable_encryption
1613 gpg_pref = gajim.config.get_per('contacts', contact.jid,
1614 'gpg_enabled')
1616 # try GPG first
1617 if not e2e_is_active and gpg_pref and \
1618 gajim.config.get_per('accounts', self.account, 'keyid') and \
1619 gajim.connections[self.account].USE_GPG:
1620 self.gpg_is_active = True
1621 gajim.encrypted_chats[self.account].append(contact.jid)
1622 msg = _('GPG encryption enabled')
1623 ChatControlBase.print_conversation_line(self, msg,
1624 'status', '', None)
1626 if self.session:
1627 self.session.loggable = gajim.config.get_per('accounts',
1628 self.account, 'log_encrypted_sessions')
1629 # GPG is always authenticated as we use GPG's WoT
1630 self._show_lock_image(self.gpg_is_active, 'GPG', self.gpg_is_active,
1631 self.session and self.session.is_loggable(), True)
1633 self.update_ui()
1634 # restore previous conversation
1635 self.restore_conversation()
1636 self.msg_textview.grab_focus()
1638 # change tooltip text for audio and video buttons if python-farsight is
1639 # not installed
1640 if not gajim.HAVE_FARSIGHT:
1641 tooltip_text = self._audio_button.get_tooltip_text()
1642 self._audio_button.set_tooltip_text(
1643 '%s\n%s' % (tooltip_text, _('Requires python-farsight.')))
1644 tooltip_text = self._video_button.get_tooltip_text()
1645 self._video_button.set_tooltip_text(
1646 '%s\n%s' % (tooltip_text, _('Requires python-farsight.')))
1648 gajim.ged.register_event_handler('pep-received', ged.GUI1,
1649 self._nec_pep_received)
1650 gajim.ged.register_event_handler('vcard-received', ged.GUI1,
1651 self._nec_vcard_received)
1652 gajim.ged.register_event_handler('failed-decrypt', ged.GUI1,
1653 self._nec_failed_decrypt)
1654 gajim.ged.register_event_handler('chatstate-received', ged.GUI1,
1655 self._nec_chatstate_received)
1656 gajim.ged.register_event_handler('caps-received', ged.GUI1,
1657 self._nec_caps_received)
1659 # PluginSystem: adding GUI extension point for this ChatControl
1660 # instance object
1661 gajim.plugin_manager.gui_extension_point('chat_control', self)
1663 def _update_toolbar(self):
1664 # Formatting
1665 if self.contact.supports(NS_XHTML_IM) and not self.gpg_is_active:
1666 self._formattings_button.set_sensitive(True)
1667 else:
1668 self._formattings_button.set_sensitive(False)
1670 # Add to roster
1671 if not isinstance(self.contact, GC_Contact) \
1672 and _('Not in Roster') in self.contact.groups:
1673 self._add_to_roster_button.show()
1674 else:
1675 self._add_to_roster_button.hide()
1677 # Jingle detection
1678 if self.contact.supports(NS_JINGLE_ICE_UDP) and \
1679 gajim.HAVE_FARSIGHT and self.contact.resource:
1680 self.audio_available = self.contact.supports(NS_JINGLE_RTP_AUDIO)
1681 self.video_available = self.contact.supports(NS_JINGLE_RTP_VIDEO)
1682 else:
1683 if self.video_available or self.audio_available:
1684 self.stop_jingle()
1685 self.video_available = False
1686 self.audio_available = False
1688 # Audio buttons
1689 self._audio_button.set_sensitive(self.audio_available)
1691 # Video buttons
1692 self._video_button.set_sensitive(self.video_available)
1694 # Send file
1695 if self.contact.supports(NS_FILE) and self.contact.resource:
1696 self._send_file_button.set_sensitive(True)
1697 self._send_file_button.set_tooltip_text('')
1698 else:
1699 self._send_file_button.set_sensitive(False)
1700 if not self.contact.supports(NS_FILE):
1701 self._send_file_button.set_tooltip_text(_(
1702 "This contact does not support file transfer."))
1703 else:
1704 self._send_file_button.set_tooltip_text(
1705 _("You need to know the real JID of the contact to send him or "
1706 "her a file."))
1708 # Convert to GC
1709 if self.contact.supports(NS_MUC):
1710 self._convert_to_gc_button.set_sensitive(True)
1711 else:
1712 self._convert_to_gc_button.set_sensitive(False)
1714 def update_all_pep_types(self):
1715 for pep_type in self._pep_images:
1716 self.update_pep(pep_type)
1718 def update_pep(self, pep_type):
1719 if isinstance(self.contact, GC_Contact):
1720 return
1721 if pep_type not in self._pep_images:
1722 return
1723 pep = self.contact.pep
1724 img = self._pep_images[pep_type]
1725 if pep_type in pep:
1726 img.set_from_pixbuf(pep[pep_type].asPixbufIcon())
1727 img.set_tooltip_markup(pep[pep_type].asMarkupText())
1728 img.show()
1729 else:
1730 img.hide()
1732 def _nec_pep_received(self, obj):
1733 if obj.conn.name != self.account:
1734 return
1735 if obj.jid != self.contact.jid:
1736 return
1738 if obj.pep_type == 'nickname':
1739 self.update_ui()
1740 self.parent_win.redraw_tab(self)
1741 self.parent_win.show_title()
1742 else:
1743 self.update_pep(obj.pep_type)
1745 def _update_jingle(self, jingle_type):
1746 if jingle_type not in ('audio', 'video'):
1747 return
1748 banner_image = getattr(self, '_' + jingle_type + '_banner_image')
1749 state = getattr(self, jingle_type + '_state')
1750 if state == self.JINGLE_STATE_NULL:
1751 banner_image.hide()
1752 else:
1753 banner_image.show()
1754 if state == self.JINGLE_STATE_CONNECTING:
1755 banner_image.set_from_stock(
1756 gtk.STOCK_CONVERT, 1)
1757 elif state == self.JINGLE_STATE_CONNECTION_RECEIVED:
1758 banner_image.set_from_stock(
1759 gtk.STOCK_NETWORK, 1)
1760 elif state == self.JINGLE_STATE_CONNECTED:
1761 banner_image.set_from_stock(
1762 gtk.STOCK_CONNECT, 1)
1763 elif state == self.JINGLE_STATE_ERROR:
1764 banner_image.set_from_stock(
1765 gtk.STOCK_DIALOG_WARNING, 1)
1766 self.update_toolbar()
1768 def update_audio(self):
1769 self._update_jingle('audio')
1770 hbox = self.xml.get_object('audio_buttons_hbox')
1771 if self.audio_state == self.JINGLE_STATE_CONNECTED:
1772 # Set volume from config
1773 input_vol = gajim.config.get('audio_input_volume')
1774 output_vol = gajim.config.get('audio_output_volume')
1775 input_vol = max(min(input_vol, 100), 0)
1776 output_vol = max(min(output_vol, 100), 0)
1777 self.xml.get_object('mic_hscale').set_value(input_vol)
1778 self.xml.get_object('sound_hscale').set_value(output_vol)
1779 # Show vbox
1780 hbox.set_no_show_all(False)
1781 hbox.show_all()
1782 elif not self.audio_sid:
1783 hbox.set_no_show_all(True)
1784 hbox.hide()
1786 def update_video(self):
1787 self._update_jingle('video')
1789 def change_resource(self, resource):
1790 old_full_jid = self.get_full_jid()
1791 self.resource = resource
1792 new_full_jid = self.get_full_jid()
1793 # update gajim.last_message_time
1794 if old_full_jid in gajim.last_message_time[self.account]:
1795 gajim.last_message_time[self.account][new_full_jid] = \
1796 gajim.last_message_time[self.account][old_full_jid]
1797 # update events
1798 gajim.events.change_jid(self.account, old_full_jid, new_full_jid)
1799 # update MessageWindow._controls
1800 self.parent_win.change_jid(self.account, old_full_jid, new_full_jid)
1802 def stop_jingle(self, sid=None, reason=None):
1803 if self.audio_sid and sid in (self.audio_sid, None):
1804 self.close_jingle_content('audio')
1805 if self.video_sid and sid in (self.video_sid, None):
1806 self.close_jingle_content('video')
1808 def _set_jingle_state(self, jingle_type, state, sid=None, reason=None):
1809 if jingle_type not in ('audio', 'video'):
1810 return
1811 if state in ('connecting', 'connected', 'stop', 'error') and reason:
1812 str = _('%(type)s state : %(state)s, reason: %(reason)s') % {
1813 'type': jingle_type.capitalize(), 'state': state, 'reason': reason}
1814 self.print_conversation(str, 'info')
1816 states = {'connecting': self.JINGLE_STATE_CONNECTING,
1817 'connection_received': self.JINGLE_STATE_CONNECTION_RECEIVED,
1818 'connected': self.JINGLE_STATE_CONNECTED,
1819 'stop': self.JINGLE_STATE_NULL,
1820 'error': self.JINGLE_STATE_ERROR}
1822 jingle_state = states[state]
1823 if getattr(self, jingle_type + '_state') == jingle_state or state == 'error':
1824 return
1826 if state == 'stop' and getattr(self, jingle_type + '_sid') not in (None, sid):
1827 return
1829 setattr(self, jingle_type + '_state', jingle_state)
1831 if jingle_state == self.JINGLE_STATE_NULL:
1832 setattr(self, jingle_type + '_sid', None)
1833 if state in ('connection_received', 'connecting'):
1834 setattr(self, jingle_type + '_sid', sid)
1836 getattr(self, '_' + jingle_type + '_button').set_active(jingle_state != self.JINGLE_STATE_NULL)
1838 getattr(self, 'update_' + jingle_type)()
1840 def set_audio_state(self, state, sid=None, reason=None):
1841 self._set_jingle_state('audio', state, sid=sid, reason=reason)
1843 def set_video_state(self, state, sid=None, reason=None):
1844 self._set_jingle_state('video', state, sid=sid, reason=reason)
1846 def _get_audio_content(self):
1847 session = gajim.connections[self.account].get_jingle_session(
1848 self.contact.get_full_jid(), self.audio_sid)
1849 return session.get_content('audio')
1851 def on_num_button_pressed(self, widget, num):
1852 self._get_audio_content()._start_dtmf(num)
1854 def on_num_button_released(self, released):
1855 self._get_audio_content()._stop_dtmf()
1857 def on_dtmf_button_clicked(self, widget):
1858 self.dtmf_window.show_all()
1860 def on_dtmf_window_focus_out_event(self, widget, event):
1861 self.dtmf_window.hide()
1863 def on_mic_hscale_value_changed(self, widget, value):
1864 self._get_audio_content().set_mic_volume(value / 100)
1865 # Save volume to config
1866 gajim.config.set('audio_input_volume', value)
1868 def on_sound_hscale_value_changed(self, widget, value):
1869 self._get_audio_content().set_out_volume(value / 100)
1870 # Save volume to config
1871 gajim.config.set('audio_output_volume', value)
1873 def on_avatar_eventbox_enter_notify_event(self, widget, event):
1875 Enter the eventbox area so we under conditions add a timeout to show a
1876 bigger avatar after 0.5 sec
1878 jid = self.contact.jid
1879 avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid)
1880 if avatar_pixbuf in ('ask', None):
1881 return
1882 avatar_w = avatar_pixbuf.get_width()
1883 avatar_h = avatar_pixbuf.get_height()
1885 scaled_buf = self.xml.get_object('avatar_image').get_pixbuf()
1886 scaled_buf_w = scaled_buf.get_width()
1887 scaled_buf_h = scaled_buf.get_height()
1889 # do we have something bigger to show?
1890 if avatar_w > scaled_buf_w or avatar_h > scaled_buf_h:
1891 # wait for 0.5 sec in case we leave earlier
1892 if self.show_bigger_avatar_timeout_id is not None:
1893 gobject.source_remove(self.show_bigger_avatar_timeout_id)
1894 self.show_bigger_avatar_timeout_id = gobject.timeout_add(500,
1895 self.show_bigger_avatar, widget)
1897 def on_avatar_eventbox_leave_notify_event(self, widget, event):
1899 Left the eventbox area that holds the avatar img
1901 # did we add a timeout? if yes remove it
1902 if self.show_bigger_avatar_timeout_id is not None:
1903 gobject.source_remove(self.show_bigger_avatar_timeout_id)
1904 self.show_bigger_avatar_timeout_id = None
1906 def on_avatar_eventbox_button_press_event(self, widget, event):
1908 If right-clicked, show popup
1910 if event.button == 3: # right click
1911 menu = gtk.Menu()
1912 menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS)
1913 id_ = menuitem.connect('activate',
1914 gtkgui_helpers.on_avatar_save_as_menuitem_activate,
1915 self.contact.jid, self.contact.get_shown_name())
1916 self.handlers[id_] = menuitem
1917 menu.append(menuitem)
1918 menu.show_all()
1919 menu.connect('selection-done', lambda w: w.destroy())
1920 # show the menu
1921 menu.show_all()
1922 menu.popup(None, None, None, event.button, event.time)
1923 return True
1925 def on_location_eventbox_button_release_event(self, widget, event):
1926 if 'location' in self.contact.pep:
1927 location = self.contact.pep['location']._pep_specific_data
1928 if ('lat' in location) and ('lon' in location):
1929 uri = 'http://www.openstreetmap.org/?' + \
1930 'mlat=%(lat)s&mlon=%(lon)s&zoom=16' % {'lat': location['lat'],
1931 'lon': location['lon']}
1932 helpers.launch_browser_mailer('url', uri)
1934 def _on_window_motion_notify(self, widget, event):
1936 It gets called no matter if it is the active window or not
1938 if self.parent_win.get_active_jid() == self.contact.jid:
1939 # if window is the active one, change vars assisting chatstate
1940 self.mouse_over_in_last_5_secs = True
1941 self.mouse_over_in_last_30_secs = True
1943 def _schedule_activity_timers(self):
1944 self.possible_paused_timeout_id = gobject.timeout_add_seconds(5,
1945 self.check_for_possible_paused_chatstate, None)
1946 self.possible_inactive_timeout_id = gobject.timeout_add_seconds(30,
1947 self.check_for_possible_inactive_chatstate, None)
1949 def update_ui(self):
1950 # The name banner is drawn here
1951 ChatControlBase.update_ui(self)
1952 self.update_toolbar()
1954 def _update_banner_state_image(self):
1955 contact = gajim.contacts.get_contact_with_highest_priority(self.account,
1956 self.contact.jid)
1957 if not contact or self.resource:
1958 # For transient contacts
1959 contact = self.contact
1960 show = contact.show
1961 jid = contact.jid
1963 # Set banner image
1964 img_32 = gajim.interface.roster.get_appropriate_state_images(jid,
1965 size='32', icon_name=show)
1966 img_16 = gajim.interface.roster.get_appropriate_state_images(jid,
1967 icon_name=show)
1968 if show in img_32 and img_32[show].get_pixbuf():
1969 # we have 32x32! use it!
1970 banner_image = img_32[show]
1971 use_size_32 = True
1972 else:
1973 banner_image = img_16[show]
1974 use_size_32 = False
1976 banner_status_img = self.xml.get_object('banner_status_image')
1977 if banner_image.get_storage_type() == gtk.IMAGE_ANIMATION:
1978 banner_status_img.set_from_animation(banner_image.get_animation())
1979 else:
1980 pix = banner_image.get_pixbuf()
1981 if pix is not None:
1982 if use_size_32:
1983 banner_status_img.set_from_pixbuf(pix)
1984 else: # we need to scale 16x16 to 32x32
1985 scaled_pix = pix.scale_simple(32, 32,
1986 gtk.gdk.INTERP_BILINEAR)
1987 banner_status_img.set_from_pixbuf(scaled_pix)
1989 def draw_banner_text(self):
1991 Draw the text in the fat line at the top of the window that houses the
1992 name, jid
1994 contact = self.contact
1995 jid = contact.jid
1997 banner_name_label = self.xml.get_object('banner_name_label')
1999 name = contact.get_shown_name()
2000 if self.resource:
2001 name += '/' + self.resource
2002 if self.TYPE_ID == message_control.TYPE_PM:
2003 name = _('%(nickname)s from group chat %(room_name)s') %\
2004 {'nickname': name, 'room_name': self.room_name}
2005 name = gobject.markup_escape_text(name)
2007 # We know our contacts nick, but if another contact has the same nick
2008 # in another account we need to also display the account.
2009 # except if we are talking to two different resources of the same contact
2010 acct_info = ''
2011 for account in gajim.contacts.get_accounts():
2012 if account == self.account:
2013 continue
2014 if acct_info: # We already found a contact with same nick
2015 break
2016 for jid in gajim.contacts.get_jid_list(account):
2017 other_contact_ = \
2018 gajim.contacts.get_first_contact_from_jid(account, jid)
2019 if other_contact_.get_shown_name() == self.contact.get_shown_name():
2020 acct_info = ' (%s)' % \
2021 gobject.markup_escape_text(self.account)
2022 break
2024 status = contact.status
2025 if status is not None:
2026 banner_name_label.set_ellipsize(pango.ELLIPSIZE_END)
2027 self.banner_status_label.set_ellipsize(pango.ELLIPSIZE_END)
2028 status_reduced = helpers.reduce_chars_newlines(status, max_lines=1)
2029 else:
2030 status_reduced = ''
2031 status_escaped = gobject.markup_escape_text(status_reduced)
2033 font_attrs, font_attrs_small = self.get_font_attrs()
2034 st = gajim.config.get('displayed_chat_state_notifications')
2035 cs = contact.chatstate
2036 if cs and st in ('composing_only', 'all'):
2037 if contact.show == 'offline':
2038 chatstate = ''
2039 elif contact.composing_xep == 'XEP-0085':
2040 if st == 'all' or cs == 'composing':
2041 chatstate = helpers.get_uf_chatstate(cs)
2042 else:
2043 chatstate = ''
2044 elif contact.composing_xep == 'XEP-0022':
2045 if cs in ('composing', 'paused'):
2046 # only print composing, paused
2047 chatstate = helpers.get_uf_chatstate(cs)
2048 else:
2049 chatstate = ''
2050 else:
2051 # When does that happen ? See [7797] and [7804]
2052 chatstate = helpers.get_uf_chatstate(cs)
2054 label_text = '<span %s>%s</span><span %s>%s %s</span>' \
2055 % (font_attrs, name, font_attrs_small,
2056 acct_info, chatstate)
2057 if acct_info:
2058 acct_info = ' ' + acct_info
2059 label_tooltip = '%s%s %s' % (name, acct_info, chatstate)
2060 else:
2061 # weight="heavy" size="x-large"
2062 label_text = '<span %s>%s</span><span %s>%s</span>' % \
2063 (font_attrs, name, font_attrs_small, acct_info)
2064 if acct_info:
2065 acct_info = ' ' + acct_info
2066 label_tooltip = '%s%s' % (name, acct_info)
2068 if status_escaped:
2069 status_text = self.urlfinder.sub(self.make_href, status_escaped)
2070 status_text = '<span %s>%s</span>' % (font_attrs_small, status_text)
2071 self.banner_status_label.set_tooltip_text(status)
2072 self.banner_status_label.set_no_show_all(False)
2073 self.banner_status_label.show()
2074 else:
2075 status_text = ''
2076 self.banner_status_label.hide()
2077 self.banner_status_label.set_no_show_all(True)
2079 self.banner_status_label.set_markup(status_text)
2080 # setup the label that holds name and jid
2081 banner_name_label.set_markup(label_text)
2082 banner_name_label.set_tooltip_text(label_tooltip)
2084 def close_jingle_content(self, jingle_type):
2085 sid = getattr(self, jingle_type + '_sid')
2086 if not sid:
2087 return
2088 setattr(self, jingle_type + '_sid', None)
2089 setattr(self, jingle_type + '_state', self.JINGLE_STATE_NULL)
2090 session = gajim.connections[self.account].get_jingle_session(
2091 self.contact.get_full_jid(), sid)
2092 if session:
2093 content = session.get_content(jingle_type)
2094 if content:
2095 session.remove_content(content.creator, content.name)
2096 getattr(self, '_' + jingle_type + '_button').set_active(False)
2097 getattr(self, 'update_' + jingle_type)()
2099 def on_jingle_button_toggled(self, widget, jingle_type):
2100 img_name = 'gajim-%s_%s' % ({'audio': 'mic', 'video': 'cam'}[jingle_type],
2101 {True: 'active', False: 'inactive'}[widget.get_active()])
2102 path_to_img = gtkgui_helpers.get_icon_path(img_name)
2104 if widget.get_active():
2105 if getattr(self, jingle_type + '_state') == \
2106 self.JINGLE_STATE_NULL:
2107 sid = getattr(gajim.connections[self.account],
2108 'start_' + jingle_type)(self.contact.get_full_jid())
2109 getattr(self, 'set_' + jingle_type + '_state')('connecting', sid)
2110 else:
2111 self.close_jingle_content(jingle_type)
2113 img = getattr(self, '_' + jingle_type + '_button').get_property('image')
2114 img.set_from_file(path_to_img)
2116 def on_audio_button_toggled(self, widget):
2117 self.on_jingle_button_toggled(widget, 'audio')
2119 def on_video_button_toggled(self, widget):
2120 self.on_jingle_button_toggled(widget, 'video')
2122 def _toggle_gpg(self):
2123 if not self.gpg_is_active and not self.contact.keyID:
2124 dialogs.ErrorDialog(_('No GPG key assigned'),
2125 _('No GPG key is assigned to this contact. So you cannot '
2126 'encrypt messages with GPG.'))
2127 return
2128 ec = gajim.encrypted_chats[self.account]
2129 if self.gpg_is_active:
2130 # Disable encryption
2131 ec.remove(self.contact.jid)
2132 self.gpg_is_active = False
2133 loggable = False
2134 msg = _('GPG encryption disabled')
2135 ChatControlBase.print_conversation_line(self, msg,
2136 'status', '', None)
2137 if self.session:
2138 self.session.loggable = True
2140 else:
2141 # Enable encryption
2142 ec.append(self.contact.jid)
2143 self.gpg_is_active = True
2144 msg = _('GPG encryption enabled')
2145 ChatControlBase.print_conversation_line(self, msg,
2146 'status', '', None)
2148 loggable = gajim.config.get_per('accounts', self.account,
2149 'log_encrypted_sessions')
2151 if self.session:
2152 self.session.loggable = loggable
2154 loggable = self.session.is_loggable()
2155 else:
2156 loggable = loggable and gajim.config.should_log(self.account,
2157 self.contact.jid)
2159 if loggable:
2160 msg = _('Session WILL be logged')
2161 else:
2162 msg = _('Session WILL NOT be logged')
2164 ChatControlBase.print_conversation_line(self, msg,
2165 'status', '', None)
2167 gajim.config.set_per('contacts', self.contact.jid,
2168 'gpg_enabled', self.gpg_is_active)
2170 self._show_lock_image(self.gpg_is_active, 'GPG',
2171 self.gpg_is_active, loggable, True)
2173 def _show_lock_image(self, visible, enc_type='', enc_enabled=False,
2174 chat_logged=False, authenticated=False):
2176 Set lock icon visibility and create tooltip
2178 #encryption %s active
2179 status_string = enc_enabled and _('is') or _('is NOT')
2180 #chat session %s be logged
2181 logged_string = chat_logged and _('will') or _('will NOT')
2183 if authenticated:
2184 #About encrypted chat session
2185 authenticated_string = _('and authenticated')
2186 img_path = gtkgui_helpers.get_icon_path('gajim-security_high')
2187 else:
2188 #About encrypted chat session
2189 authenticated_string = _('and NOT authenticated')
2190 img_path = gtkgui_helpers.get_icon_path('gajim-security_low')
2191 self.lock_image.set_from_file(img_path)
2193 #status will become 'is' or 'is not', authentificaed will become
2194 #'and authentificated' or 'and not authentificated', logged will become
2195 #'will' or 'will not'
2196 tooltip = _('%(type)s encryption %(status)s active %(authenticated)s.\n'
2197 'Your chat session %(logged)s be logged.') % {'type': enc_type,
2198 'status': status_string, 'authenticated': authenticated_string,
2199 'logged': logged_string}
2201 self.authentication_button.set_tooltip_text(tooltip)
2202 self.widget_set_visible(self.authentication_button, not visible)
2203 self.lock_image.set_sensitive(enc_enabled)
2205 def _on_authentication_button_clicked(self, widget):
2206 if self.gpg_is_active:
2207 dialogs.GPGInfoWindow(self)
2208 elif self.session and self.session.enable_encryption:
2209 dialogs.ESessionInfoWindow(self.session)
2211 def send_message(self, message, keyID='', chatstate=None, xhtml=None,
2212 process_commands=True):
2214 Send a message to contact
2216 message = helpers.remove_invalid_xml_chars(message)
2217 if message in ('', None, '\n'):
2218 return None
2220 # refresh timers
2221 self.reset_kbd_mouse_timeout_vars()
2223 contact = self.contact
2225 encrypted = bool(self.session) and self.session.enable_encryption
2227 keyID = ''
2228 if self.gpg_is_active:
2229 keyID = contact.keyID
2230 encrypted = True
2231 if not keyID:
2232 keyID = 'UNKNOWN'
2234 chatstates_on = gajim.config.get('outgoing_chat_state_notifications') != \
2235 'disabled'
2236 composing_xep = contact.composing_xep
2237 chatstate_to_send = None
2238 if chatstates_on and contact is not None:
2239 if composing_xep is None:
2240 # no info about peer
2241 # send active to discover chat state capabilities
2242 # this is here (and not in send_chatstate)
2243 # because we want it sent with REAL message
2244 # (not standlone) eg. one that has body
2246 if contact.our_chatstate:
2247 # We already asked for xep 85, don't ask it twice
2248 composing_xep = 'asked_once'
2250 chatstate_to_send = 'active'
2251 contact.our_chatstate = 'ask' # pseudo state
2252 # if peer supports jep85 and we are not 'ask', send 'active'
2253 # NOTE: first active and 'ask' is set in gajim.py
2254 elif composing_xep is not False:
2255 # send active chatstate on every message (as XEP says)
2256 chatstate_to_send = 'active'
2257 contact.our_chatstate = 'active'
2259 gobject.source_remove(self.possible_paused_timeout_id)
2260 gobject.source_remove(self.possible_inactive_timeout_id)
2261 self._schedule_activity_timers()
2263 def _on_sent(id_, contact, message, encrypted, xhtml, label):
2264 if contact.supports(NS_RECEIPTS) and gajim.config.get_per('accounts',
2265 self.account, 'request_receipt'):
2266 xep0184_id = id_
2267 else:
2268 xep0184_id = None
2269 if label:
2270 displaymarking = label.getTag('displaymarking')
2271 else:
2272 displaymarking = None
2273 self.print_conversation(message, self.contact.jid, encrypted=encrypted,
2274 xep0184_id=xep0184_id, xhtml=xhtml, displaymarking=displaymarking)
2276 ChatControlBase.send_message(self, message, keyID, type_='chat',
2277 chatstate=chatstate_to_send, composing_xep=composing_xep,
2278 xhtml=xhtml, callback=_on_sent,
2279 callback_args=[contact, message, encrypted, xhtml, self.get_seclabel()],
2280 process_commands=process_commands)
2283 def on_message_sent(self, account_and_message):
2284 # this is called when an external application sends a chat
2285 # message using DBus. So we likely need to update the UI
2286 # accordingly.
2287 message = account_and_message[1][1]
2288 jid_and_resource = account_and_message[1][0]
2289 if not message:
2290 return
2292 # try to filter based on jid/resource to avoid duplicate
2293 # messages.
2294 if jid_and_resource.find('/') > -1:
2295 jid = jid_and_resource.split('/')[0]
2296 if jid == self.contact.jid:
2297 self.print_conversation(message, frm='outgoing')
2299 def check_for_possible_paused_chatstate(self, arg):
2301 Did we move mouse of that window or write something in message textview
2302 in the last 5 seconds? If yes - we go active for mouse, composing for
2303 kbd. If not - we go paused if we were previously composing
2305 contact = self.contact
2306 jid = contact.jid
2307 current_state = contact.our_chatstate
2308 if current_state is False: # jid doesn't support chatstates
2309 return False # stop looping
2311 message_buffer = self.msg_textview.get_buffer()
2312 if self.kbd_activity_in_last_5_secs and message_buffer.get_char_count():
2313 # Only composing if the keyboard activity was in text entry
2314 self.send_chatstate('composing')
2315 elif self.mouse_over_in_last_5_secs and current_state == 'inactive' and\
2316 jid == self.parent_win.get_active_jid():
2317 self.send_chatstate('active')
2318 else:
2319 if current_state == 'composing':
2320 self.send_chatstate('paused') # pause composing
2322 # assume no activity and let the motion-notify or 'insert-text' make them
2323 # True refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds!
2324 self.reset_kbd_mouse_timeout_vars()
2325 return True # loop forever
2327 def check_for_possible_inactive_chatstate(self, arg):
2329 Did we move mouse over that window or wrote something in message textview
2330 in the last 30 seconds? if yes - we go active. If no - we go inactive
2332 contact = self.contact
2334 current_state = contact.our_chatstate
2335 if current_state is False: # jid doesn't support chatstates
2336 return False # stop looping
2338 if self.mouse_over_in_last_5_secs or self.kbd_activity_in_last_5_secs:
2339 return True # loop forever
2341 if not self.mouse_over_in_last_30_secs or \
2342 self.kbd_activity_in_last_30_secs:
2343 self.send_chatstate('inactive', contact)
2345 # assume no activity and let the motion-notify or 'insert-text' make them
2346 # True refresh 30 seconds too or else it's 30 - 5 = 25 seconds!
2347 self.reset_kbd_mouse_timeout_vars()
2348 return True # loop forever
2350 def reset_kbd_mouse_timeout_vars(self):
2351 self.kbd_activity_in_last_5_secs = False
2352 self.mouse_over_in_last_5_secs = False
2353 self.mouse_over_in_last_30_secs = False
2354 self.kbd_activity_in_last_30_secs = False
2356 def on_cancel_session_negotiation(self):
2357 msg = _('Session negotiation cancelled')
2358 ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
2360 def print_archiving_session_details(self):
2362 Print esession settings to textview
2364 archiving = bool(self.session) and isinstance(self.session,
2365 ArchivingStanzaSession) and self.session.archiving
2366 if archiving:
2367 msg = _('This session WILL be archived on server')
2368 else:
2369 msg = _('This session WILL NOT be archived on server')
2370 ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
2372 def print_esession_details(self):
2374 Print esession settings to textview
2376 e2e_is_active = bool(self.session) and self.session.enable_encryption
2377 if e2e_is_active:
2378 msg = _('This session is encrypted')
2380 if self.session.is_loggable():
2381 msg += _(' and WILL be logged')
2382 else:
2383 msg += _(' and WILL NOT be logged')
2385 ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
2387 if not self.session.verified_identity:
2388 ChatControlBase.print_conversation_line(self, _("Remote contact's identity not verified. Click the shield button for more details."), 'status', '', None)
2389 else:
2390 msg = _('E2E encryption disabled')
2391 ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
2393 self._show_lock_image(e2e_is_active, 'E2E', e2e_is_active, self.session and \
2394 self.session.is_loggable(), self.session and self.session.verified_identity)
2396 def print_session_details(self):
2397 if isinstance(self.session, EncryptedStanzaSession):
2398 self.print_esession_details()
2399 elif isinstance(self.session, ArchivingStanzaSession):
2400 self.print_archiving_session_details()
2402 def print_conversation(self, text, frm='', tim=None, encrypted=False,
2403 subject=None, xhtml=None, simple=False, xep0184_id=None,
2404 displaymarking=None):
2406 Print a line in the conversation
2408 If frm is set to status: it's a status message.
2409 if frm is set to error: it's an error message. The difference between
2410 status and error is mainly that with error, msg count as a new message
2411 (in systray and in control).
2412 If frm is set to info: it's a information message.
2413 If frm is set to print_queue: it is incomming from queue.
2414 If frm is set to another value: it's an outgoing message.
2415 If frm is not set: it's an incomming message.
2417 contact = self.contact
2419 if frm == 'status':
2420 if not gajim.config.get('print_status_in_chats'):
2421 return
2422 kind = 'status'
2423 name = ''
2424 elif frm == 'error':
2425 kind = 'error'
2426 name = ''
2427 elif frm == 'info':
2428 kind = 'info'
2429 name = ''
2430 else:
2431 if self.session and self.session.enable_encryption:
2432 # ESessions
2433 if not encrypted:
2434 msg = _('The following message was NOT encrypted')
2435 ChatControlBase.print_conversation_line(self, msg, 'status', '',
2436 tim)
2437 else:
2438 # GPG encryption
2439 if encrypted and not self.gpg_is_active:
2440 msg = _('The following message was encrypted')
2441 ChatControlBase.print_conversation_line(self, msg, 'status', '',
2442 tim)
2443 # turn on OpenPGP if this was in fact a XEP-0027 encrypted message
2444 if encrypted == 'xep27':
2445 self._toggle_gpg()
2446 elif not encrypted and self.gpg_is_active:
2447 msg = _('The following message was NOT encrypted')
2448 ChatControlBase.print_conversation_line(self, msg, 'status', '',
2449 tim)
2450 if not frm:
2451 kind = 'incoming'
2452 name = contact.get_shown_name()
2453 elif frm == 'print_queue': # incoming message, but do not update time
2454 kind = 'incoming_queue'
2455 name = contact.get_shown_name()
2456 else:
2457 kind = 'outgoing'
2458 name = gajim.nicks[self.account]
2459 if not xhtml and not (encrypted and self.gpg_is_active) and \
2460 gajim.config.get('rst_formatting_outgoing_messages'):
2461 from common.rst_xhtml_generator import create_xhtml
2462 xhtml = create_xhtml(text)
2463 if xhtml:
2464 xhtml = '<body xmlns="%s">%s</body>' % (NS_XHTML, xhtml)
2465 ChatControlBase.print_conversation_line(self, text, kind, name, tim,
2466 subject=subject, old_kind=self.old_msg_kind, xhtml=xhtml,
2467 simple=simple, xep0184_id=xep0184_id, displaymarking=displaymarking)
2468 if text.startswith('/me ') or text.startswith('/me\n'):
2469 self.old_msg_kind = None
2470 else:
2471 self.old_msg_kind = kind
2473 def get_tab_label(self, chatstate):
2474 unread = ''
2475 if self.resource:
2476 jid = self.contact.get_full_jid()
2477 else:
2478 jid = self.contact.jid
2479 num_unread = len(gajim.events.get_events(self.account, jid,
2480 ['printed_' + self.type_id, self.type_id]))
2481 if num_unread == 1 and not gajim.config.get('show_unread_tab_icon'):
2482 unread = '*'
2483 elif num_unread > 1:
2484 unread = '[' + unicode(num_unread) + ']'
2486 # Draw tab label using chatstate
2487 theme = gajim.config.get('roster_theme')
2488 color = None
2489 if not chatstate:
2490 chatstate = self.contact.chatstate
2491 if chatstate is not None:
2492 if chatstate == 'composing':
2493 color = gajim.config.get_per('themes', theme,
2494 'state_composing_color')
2495 elif chatstate == 'inactive':
2496 color = gajim.config.get_per('themes', theme,
2497 'state_inactive_color')
2498 elif chatstate == 'gone':
2499 color = gajim.config.get_per('themes', theme,
2500 'state_gone_color')
2501 elif chatstate == 'paused':
2502 color = gajim.config.get_per('themes', theme,
2503 'state_paused_color')
2504 if color:
2505 # We set the color for when it's the current tab or not
2506 color = gtk.gdk.colormap_get_system().alloc_color(color)
2507 # In inactive tab color to be lighter against the darker inactive
2508 # background
2509 if chatstate in ('inactive', 'gone') and\
2510 self.parent_win.get_active_control() != self:
2511 color = self.lighten_color(color)
2512 else: # active or not chatstate, get color from gtk
2513 color = self.parent_win.notebook.style.fg[gtk.STATE_ACTIVE]
2515 name = self.contact.get_shown_name()
2516 if self.resource:
2517 name += '/' + self.resource
2518 label_str = gobject.markup_escape_text(name)
2519 if num_unread: # if unread, text in the label becomes bold
2520 label_str = '<b>' + unread + label_str + '</b>'
2521 return (label_str, color)
2523 def get_tab_image(self, count_unread=True):
2524 if self.resource:
2525 jid = self.contact.get_full_jid()
2526 else:
2527 jid = self.contact.jid
2528 if count_unread:
2529 num_unread = len(gajim.events.get_events(self.account, jid,
2530 ['printed_' + self.type_id, self.type_id]))
2531 else:
2532 num_unread = 0
2533 # Set tab image (always 16x16); unread messages show the 'event' image
2534 tab_img = None
2536 if num_unread and gajim.config.get('show_unread_tab_icon'):
2537 img_16 = gajim.interface.roster.get_appropriate_state_images(
2538 self.contact.jid, icon_name='event')
2539 tab_img = img_16['event']
2540 else:
2541 contact = gajim.contacts.get_contact_with_highest_priority(
2542 self.account, self.contact.jid)
2543 if not contact or self.resource:
2544 # For transient contacts
2545 contact = self.contact
2546 img_16 = gajim.interface.roster.get_appropriate_state_images(
2547 self.contact.jid, icon_name=contact.show)
2548 tab_img = img_16[contact.show]
2550 return tab_img
2552 def prepare_context_menu(self, hide_buttonbar_items=False):
2554 Set compact view menuitem active state sets active and sensitivity state
2555 for toggle_gpg_menuitem sets sensitivity for history_menuitem (False for
2556 tranasports) and file_transfer_menuitem and hide()/show() for
2557 add_to_roster_menuitem
2559 menu = gui_menu_builder.get_contact_menu(self.contact, self.account,
2560 use_multiple_contacts=False, show_start_chat=False,
2561 show_encryption=True, control=self,
2562 show_buttonbar_items=not hide_buttonbar_items)
2563 return menu
2565 def send_chatstate(self, state, contact=None):
2567 Send OUR chatstate as STANDLONE chat state message (eg. no body)
2568 to contact only if new chatstate is different from the previous one
2569 if jid is not specified, send to active tab
2571 # JEP 85 does not allow resending the same chatstate
2572 # this function checks for that and just returns so it's safe to call it
2573 # with same state.
2575 # This functions also checks for violation in state transitions
2576 # and raises RuntimeException with appropriate message
2577 # more on that http://xmpp.org/extensions/xep-0085.html#statechart
2579 # do not send nothing if we have chat state notifications disabled
2580 # that means we won't reply to the <active/> from other peer
2581 # so we do not broadcast jep85 capabalities
2582 chatstate_setting = gajim.config.get('outgoing_chat_state_notifications')
2583 if chatstate_setting == 'disabled':
2584 return
2585 elif chatstate_setting == 'composing_only' and state != 'active' and\
2586 state != 'composing':
2587 return
2589 if contact is None:
2590 contact = self.parent_win.get_active_contact()
2591 if contact is None:
2592 # contact was from pm in MUC, and left the room so contact is None
2593 # so we cannot send chatstate anymore
2594 return
2596 # Don't send chatstates to offline contacts
2597 if contact.show == 'offline':
2598 return
2600 if contact.composing_xep is False: # jid cannot do xep85 nor xep22
2601 return
2603 # if the new state we wanna send (state) equals
2604 # the current state (contact.our_chatstate) then return
2605 if contact.our_chatstate == state:
2606 return
2608 if contact.composing_xep is None:
2609 # we don't know anything about jid, so return
2610 # NOTE:
2611 # send 'active', set current state to 'ask' and return is done
2612 # in self.send_message() because we need REAL message (with <body>)
2613 # for that procedure so return to make sure we send only once
2614 # 'active' until we know peer supports jep85
2615 return
2617 if contact.our_chatstate == 'ask':
2618 return
2620 # in JEP22, when we already sent stop composing
2621 # notification on paused, don't resend it
2622 if contact.composing_xep == 'XEP-0022' and \
2623 contact.our_chatstate in ('paused', 'active', 'inactive') and \
2624 state is not 'composing': # not composing == in (active, inactive, gone)
2625 contact.our_chatstate = 'active'
2626 self.reset_kbd_mouse_timeout_vars()
2627 return
2629 # if we're inactive prevent composing (JEP violation)
2630 if contact.our_chatstate == 'inactive' and state == 'composing':
2631 # go active before
2632 gajim.nec.push_outgoing_event(MessageOutgoingEvent(None,
2633 account=self.account, jid=self.contact.jid, chatstate='active'))
2634 contact.our_chatstate = 'active'
2635 self.reset_kbd_mouse_timeout_vars()
2637 gajim.nec.push_outgoing_event(MessageOutgoingEvent(None,
2638 account=self.account, jid=self.contact.jid, chatstate=state,
2639 msg_id=contact.msg_id, composing_xep=contact.composing_xep))
2641 contact.our_chatstate = state
2642 if contact.our_chatstate == 'active':
2643 self.reset_kbd_mouse_timeout_vars()
2645 def shutdown(self):
2646 # PluginSystem: removing GUI extension points connected with ChatControl
2647 # instance object
2648 gajim.plugin_manager.remove_gui_extension_point('chat_control', self)
2650 # disconnect from the dbus MessageSent signal.
2651 if self._dbus_message_sent_match:
2652 self._dbus_message_sent_match.remove()
2654 gajim.ged.remove_event_handler('pep-received', ged.GUI1,
2655 self._nec_pep_received)
2656 gajim.ged.remove_event_handler('vcard-received', ged.GUI1,
2657 self._nec_vcard_received)
2658 gajim.ged.remove_event_handler('failed-decrypt', ged.GUI1,
2659 self._nec_failed_decrypt)
2660 gajim.ged.remove_event_handler('chatstate-received', ged.GUI1,
2661 self._nec_chatstate_received)
2662 gajim.ged.remove_event_handler('caps-received', ged.GUI1,
2663 self._nec_caps_received)
2665 # Send 'gone' chatstate
2666 self.send_chatstate('gone', self.contact)
2667 self.contact.chatstate = None
2668 self.contact.our_chatstate = None
2670 for jingle_type in ('audio', 'video'):
2671 self.close_jingle_content(jingle_type)
2673 # disconnect self from session
2674 if self.session:
2675 self.session.control = None
2677 # Disconnect timer callbacks
2678 gobject.source_remove(self.possible_paused_timeout_id)
2679 gobject.source_remove(self.possible_inactive_timeout_id)
2680 # Remove bigger avatar window
2681 if self.bigger_avatar_window:
2682 self.bigger_avatar_window.destroy()
2683 # Clean events
2684 gajim.events.remove_events(self.account, self.get_full_jid(),
2685 types=['printed_' + self.type_id, self.type_id])
2686 # Remove contact instance if contact has been removed
2687 key = (self.contact.jid, self.account)
2688 roster = gajim.interface.roster
2689 if key in roster.contacts_to_be_removed.keys() and \
2690 not roster.contact_has_pending_roster_events(self.contact,
2691 self.account):
2692 backend = roster.contacts_to_be_removed[key]['backend']
2693 del roster.contacts_to_be_removed[key]
2694 roster.remove_contact(self.contact.jid, self.account, force=True,
2695 backend=backend)
2696 # remove all register handlers on widgets, created by self.xml
2697 # to prevent circular references among objects
2698 for i in self.handlers.keys():
2699 if self.handlers[i].handler_is_connected(i):
2700 self.handlers[i].disconnect(i)
2701 del self.handlers[i]
2702 self.conv_textview.del_handlers()
2703 if gajim.config.get('use_speller') and HAS_GTK_SPELL:
2704 spell_obj = gtkspell.get_from_text_view(self.msg_textview)
2705 if spell_obj:
2706 spell_obj.detach()
2707 self.msg_textview.destroy()
2708 # PluginSystem: calling shutdown of super class (ChatControlBase) to let
2709 # it remove it's GUI extension points
2710 super(ChatControl, self).shutdown()
2712 def minimizable(self):
2713 return False
2715 def safe_shutdown(self):
2716 return False
2718 def allow_shutdown(self, method, on_yes, on_no, on_minimize):
2719 if time.time() - gajim.last_message_time[self.account]\
2720 [self.get_full_jid()] < 2:
2721 # 2 seconds
2723 def on_ok():
2724 on_yes(self)
2726 def on_cancel():
2727 on_no(self)
2729 dialogs.ConfirmationDialog(
2730 # %s is being replaced in the code with JID
2731 _('You just received a new message from "%s"') % self.contact.jid,
2732 _('If you close this tab and you have history disabled, '\
2733 'this message will be lost.'), on_response_ok=on_ok,
2734 on_response_cancel=on_cancel)
2735 return
2736 on_yes(self)
2738 def _nec_chatstate_received(self, obj):
2740 Handle incoming chatstate that jid SENT TO us
2742 self.draw_banner_text()
2743 # update chatstate in tab for this chat
2744 self.parent_win.redraw_tab(self, self.contact.chatstate)
2746 def _nec_caps_received(self, obj):
2747 if obj.conn.name != self.account or obj.jid != self.contact.jid:
2748 return
2749 self.update_ui()
2751 def set_control_active(self, state):
2752 ChatControlBase.set_control_active(self, state)
2753 # send chatstate inactive to the one we're leaving
2754 # and active to the one we visit
2755 if state:
2756 message_buffer = self.msg_textview.get_buffer()
2757 if message_buffer.get_char_count():
2758 self.send_chatstate('paused', self.contact)
2759 else:
2760 self.send_chatstate('active', self.contact)
2761 self.reset_kbd_mouse_timeout_vars()
2762 gobject.source_remove(self.possible_paused_timeout_id)
2763 gobject.source_remove(self.possible_inactive_timeout_id)
2764 self._schedule_activity_timers()
2765 else:
2766 self.send_chatstate('inactive', self.contact)
2767 # Hide bigger avatar window
2768 if self.bigger_avatar_window:
2769 self.bigger_avatar_window.destroy()
2770 self.bigger_avatar_window = None
2771 # Re-show the small avatar
2772 self.show_avatar()
2774 def show_avatar(self):
2775 if not gajim.config.get('show_avatar_in_chat'):
2776 return
2778 jid_with_resource = self.contact.get_full_jid()
2779 pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid_with_resource)
2780 if pixbuf == 'ask':
2781 # we don't have the vcard
2782 if self.TYPE_ID == message_control.TYPE_PM:
2783 if self.gc_contact.jid:
2784 # We know the real jid of this contact
2785 real_jid = self.gc_contact.jid
2786 if self.gc_contact.resource:
2787 real_jid += '/' + self.gc_contact.resource
2788 else:
2789 real_jid = jid_with_resource
2790 gajim.connections[self.account].request_vcard(real_jid,
2791 jid_with_resource)
2792 else:
2793 gajim.connections[self.account].request_vcard(jid_with_resource)
2794 return
2795 elif pixbuf:
2796 scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'chat')
2797 else:
2798 scaled_pixbuf = None
2800 image = self.xml.get_object('avatar_image')
2801 image.set_from_pixbuf(scaled_pixbuf)
2802 image.show_all()
2804 def _nec_vcard_received(self, obj):
2805 if obj.conn.name != self.account:
2806 return
2807 j = gajim.get_jid_without_resource(self.contact.jid)
2808 if obj.jid != j:
2809 return
2810 self.show_avatar()
2812 def _on_drag_data_received(self, widget, context, x, y, selection,
2813 target_type, timestamp):
2814 if not selection.data:
2815 return
2816 if self.TYPE_ID == message_control.TYPE_PM:
2817 c = self.gc_contact
2818 else:
2819 c = self.contact
2820 if target_type == self.TARGET_TYPE_URI_LIST:
2821 if not c.resource: # If no resource is known, we can't send a file
2822 return
2823 uri = selection.data.strip()
2824 uri_splitted = uri.split() # we may have more than one file dropped
2825 for uri in uri_splitted:
2826 path = helpers.get_file_path_from_dnd_dropped_uri(uri)
2827 if os.path.isfile(path): # is it file?
2828 ft = gajim.interface.instances['file_transfers']
2829 ft.send_file(self.account, c, path)
2830 return
2832 # chat2muc
2833 treeview = gajim.interface.roster.tree
2834 model = treeview.get_model()
2835 data = selection.data
2836 path = treeview.get_selection().get_selected_rows()[1][0]
2837 iter_ = model.get_iter(path)
2838 type_ = model[iter_][2]
2839 if type_ != 'contact': # source is not a contact
2840 return
2841 dropped_jid = data.decode('utf-8')
2843 dropped_transport = gajim.get_transport_name_from_jid(dropped_jid)
2844 c_transport = gajim.get_transport_name_from_jid(c.jid)
2845 if dropped_transport or c_transport:
2846 return # transport contacts cannot be invited
2848 dialogs.TransformChatToMUC(self.account, [c.jid], [dropped_jid])
2850 def _on_message_tv_buffer_changed(self, textbuffer):
2851 self.kbd_activity_in_last_5_secs = True
2852 self.kbd_activity_in_last_30_secs = True
2853 if textbuffer.get_char_count():
2854 self.send_chatstate('composing', self.contact)
2856 e2e_is_active = self.session and \
2857 self.session.enable_encryption
2858 e2e_pref = gajim.config.get_per('accounts', self.account,
2859 'enable_esessions') and gajim.config.get_per('accounts',
2860 self.account, 'autonegotiate_esessions') and gajim.config.get_per(
2861 'contacts', self.contact.jid, 'autonegotiate_esessions')
2862 want_e2e = not e2e_is_active and not self.gpg_is_active \
2863 and e2e_pref
2865 if want_e2e and not self.no_autonegotiation \
2866 and gajim.HAVE_PYCRYPTO and self.contact.supports(NS_ESESSION):
2867 self.begin_e2e_negotiation()
2868 elif (not self.session or not self.session.status) and \
2869 gajim.connections[self.account].archiving_supported:
2870 self.begin_archiving_negotiation()
2871 else:
2872 self.send_chatstate('active', self.contact)
2874 def restore_conversation(self):
2875 jid = self.contact.jid
2876 # don't restore lines if it's a transport
2877 if gajim.jid_is_transport(jid):
2878 return
2880 # How many lines to restore and when to time them out
2881 restore_how_many = gajim.config.get('restore_lines')
2882 if restore_how_many <= 0:
2883 return
2884 timeout = gajim.config.get('restore_timeout') # in minutes
2886 # number of messages that are in queue and are already logged, we want
2887 # to avoid duplication
2888 pending_how_many = len(gajim.events.get_events(self.account, jid,
2889 ['chat', 'pm']))
2890 if self.resource:
2891 pending_how_many += len(gajim.events.get_events(self.account,
2892 self.contact.get_full_jid(), ['chat', 'pm']))
2894 try:
2895 rows = gajim.logger.get_last_conversation_lines(jid, restore_how_many,
2896 pending_how_many, timeout, self.account)
2897 except exceptions.DatabaseMalformed:
2898 import common.logger
2899 dialogs.ErrorDialog(_('Database Error'),
2900 _('The database file (%s) cannot be read. Try to repair it or remove it (all history will be lost).') % common.logger.LOG_DB_PATH)
2901 rows = []
2902 local_old_kind = None
2903 for row in rows: # row[0] time, row[1] has kind, row[2] the message
2904 if not row[2]: # message is empty, we don't print it
2905 continue
2906 if row[1] in (constants.KIND_CHAT_MSG_SENT,
2907 constants.KIND_SINGLE_MSG_SENT):
2908 kind = 'outgoing'
2909 name = gajim.nicks[self.account]
2910 elif row[1] in (constants.KIND_SINGLE_MSG_RECV,
2911 constants.KIND_CHAT_MSG_RECV):
2912 kind = 'incoming'
2913 name = self.contact.get_shown_name()
2914 elif row[1] == constants.KIND_ERROR:
2915 kind = 'status'
2916 name = self.contact.get_shown_name()
2918 tim = time.localtime(float(row[0]))
2920 if gajim.config.get('restored_messages_small'):
2921 small_attr = ['small']
2922 else:
2923 small_attr = []
2924 ChatControlBase.print_conversation_line(self, row[2], kind, name, tim,
2925 small_attr,
2926 small_attr + ['restored_message'],
2927 small_attr + ['restored_message'],
2928 False, old_kind=local_old_kind)
2929 if row[2].startswith('/me ') or row[2].startswith('/me\n'):
2930 local_old_kind = None
2931 else:
2932 local_old_kind = kind
2933 if len(rows):
2934 self.conv_textview.print_empty_line()
2936 def read_queue(self):
2938 Read queue and print messages containted in it
2940 jid = self.contact.jid
2941 jid_with_resource = jid
2942 if self.resource:
2943 jid_with_resource += '/' + self.resource
2944 events = gajim.events.get_events(self.account, jid_with_resource)
2946 # list of message ids which should be marked as read
2947 message_ids = []
2948 for event in events:
2949 if event.type_ != self.type_id:
2950 continue
2951 data = event.parameters
2952 kind = data[2]
2953 if kind == 'error':
2954 kind = 'info'
2955 else:
2956 kind = 'print_queue'
2957 dm = None
2958 if len(data) > 10:
2959 dm = data[10]
2960 self.print_conversation(data[0], kind, tim=data[3],
2961 encrypted=data[4], subject=data[1], xhtml=data[7],
2962 displaymarking=dm)
2963 if len(data) > 6 and isinstance(data[6], int):
2964 message_ids.append(data[6])
2966 if len(data) > 8 and not self.session:
2967 self.set_session(data[8])
2968 if message_ids:
2969 gajim.logger.set_read_messages(message_ids)
2970 gajim.events.remove_events(self.account, jid_with_resource,
2971 types=[self.type_id])
2973 typ = 'chat' # Is it a normal chat or a pm ?
2975 # reset to status image in gc if it is a pm
2976 # Is it a pm ?
2977 room_jid, nick = gajim.get_room_and_nick_from_fjid(jid)
2978 control = gajim.interface.msg_win_mgr.get_gc_control(room_jid,
2979 self.account)
2980 if control and control.type_id == message_control.TYPE_GC:
2981 control.update_ui()
2982 control.parent_win.show_title()
2983 typ = 'pm'
2985 self.redraw_after_event_removed(jid)
2986 if (self.contact.show in ('offline', 'error')):
2987 show_offline = gajim.config.get('showoffline')
2988 show_transports = gajim.config.get('show_transports_group')
2989 if (not show_transports and gajim.jid_is_transport(jid)) or \
2990 (not show_offline and typ == 'chat' and \
2991 len(gajim.contacts.get_contacts(self.account, jid)) < 2):
2992 gajim.interface.roster.remove_to_be_removed(self.contact.jid,
2993 self.account)
2994 elif typ == 'pm':
2995 control.remove_contact(nick)
2997 def show_bigger_avatar(self, small_avatar):
2999 Resize the avatar, if needed, so it has at max half the screen size and
3000 shows it
3002 if not small_avatar.window:
3003 # Tab has been closed since we hovered the avatar
3004 return
3005 avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(
3006 self.contact.jid)
3007 if avatar_pixbuf in ('ask', None):
3008 return
3009 # Hide the small avatar
3010 # this code hides the small avatar when we show a bigger one in case
3011 # the avatar has a transparency hole in the middle
3012 # so when we show the big one we avoid seeing the small one behind.
3013 # It's why I set it transparent.
3014 image = self.xml.get_object('avatar_image')
3015 pixbuf = image.get_pixbuf()
3016 pixbuf.fill(0xffffff00L) # RGBA
3017 image.queue_draw()
3019 screen_w = gtk.gdk.screen_width()
3020 screen_h = gtk.gdk.screen_height()
3021 avatar_w = avatar_pixbuf.get_width()
3022 avatar_h = avatar_pixbuf.get_height()
3023 half_scr_w = screen_w / 2
3024 half_scr_h = screen_h / 2
3025 if avatar_w > half_scr_w:
3026 avatar_w = half_scr_w
3027 if avatar_h > half_scr_h:
3028 avatar_h = half_scr_h
3029 window = gtk.Window(gtk.WINDOW_POPUP)
3030 self.bigger_avatar_window = window
3031 pixmap, mask = avatar_pixbuf.render_pixmap_and_mask()
3032 window.set_size_request(avatar_w, avatar_h)
3033 # we should make the cursor visible
3034 # gtk+ doesn't make use of the motion notify on gtkwindow by default
3035 # so this line adds that
3036 window.set_events(gtk.gdk.POINTER_MOTION_MASK)
3037 window.set_app_paintable(True)
3038 window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_TOOLTIP)
3040 window.realize()
3041 window.window.set_back_pixmap(pixmap, False) # make it transparent
3042 window.window.shape_combine_mask(mask, 0, 0)
3044 # make the bigger avatar window show up centered
3045 x0, y0 = small_avatar.window.get_origin()
3046 x0 += small_avatar.allocation.x
3047 y0 += small_avatar.allocation.y
3048 center_x= x0 + (small_avatar.allocation.width / 2)
3049 center_y = y0 + (small_avatar.allocation.height / 2)
3050 pos_x, pos_y = center_x - (avatar_w / 2), center_y - (avatar_h / 2)
3051 window.move(pos_x, pos_y)
3052 # make the cursor invisible so we can see the image
3053 invisible_cursor = gtkgui_helpers.get_invisible_cursor()
3054 window.window.set_cursor(invisible_cursor)
3056 # we should hide the window
3057 window.connect('leave_notify_event',
3058 self._on_window_avatar_leave_notify_event)
3059 window.connect('motion-notify-event',
3060 self._on_window_motion_notify_event)
3062 window.show_all()
3064 def _on_window_avatar_leave_notify_event(self, widget, event):
3066 Just left the popup window that holds avatar
3068 self.bigger_avatar_window.destroy()
3069 self.bigger_avatar_window = None
3070 # Re-show the small avatar
3071 self.show_avatar()
3073 def _on_window_motion_notify_event(self, widget, event):
3075 Just moved the mouse so show the cursor
3077 cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)
3078 self.bigger_avatar_window.window.set_cursor(cursor)
3080 def _on_send_file_menuitem_activate(self, widget):
3081 self._on_send_file()
3083 def _on_add_to_roster_menuitem_activate(self, widget):
3084 dialogs.AddNewContactWindow(self.account, self.contact.jid)
3086 def _on_contact_information_menuitem_activate(self, widget):
3087 gajim.interface.roster.on_info(widget, self.contact, self.account)
3089 def _on_toggle_gpg_menuitem_activate(self, widget):
3090 self._toggle_gpg()
3092 def _on_convert_to_gc_menuitem_activate(self, widget):
3094 User wants to invite some friends to chat
3096 dialogs.TransformChatToMUC(self.account, [self.contact.jid])
3098 def _on_toggle_e2e_menuitem_activate(self, widget):
3099 if self.session and self.session.enable_encryption:
3100 # e2e was enabled, disable it
3101 jid = str(self.session.jid)
3102 thread_id = self.session.thread_id
3104 self.session.terminate_e2e()
3106 gajim.connections[self.account].delete_session(jid, thread_id)
3108 # presumably the user had a good reason to shut it off, so
3109 # disable autonegotiation too
3110 self.no_autonegotiation = True
3111 else:
3112 self.begin_e2e_negotiation()
3114 def begin_negotiation(self):
3115 self.no_autonegotiation = True
3117 if not self.session:
3118 fjid = self.contact.get_full_jid()
3119 new_sess = gajim.connections[self.account].make_new_session(fjid, type_=self.type_id)
3120 self.set_session(new_sess)
3122 def begin_e2e_negotiation(self):
3123 self.begin_negotiation()
3124 self.session.negotiate_e2e(False)
3126 def begin_archiving_negotiation(self):
3127 self.begin_negotiation()
3128 self.session.negotiate_archiving()
3130 def _nec_failed_decrypt(self, obj):
3131 if obj.session != self.session:
3132 return
3134 details = _('Unable to decrypt message from %s\nIt may have been '
3135 'tampered with.') % obj.fjid
3136 self.print_conversation_line(details, 'status', '', obj.timestamp)
3138 # terminate the session
3139 thread_id = self.session.thread_id
3140 self.session.terminate_e2e()
3141 obj.conn.delete_session(obj.fjid, thread_id)
3143 # restart the session
3144 self.begin_e2e_negotiation()
3146 # Stop emission so it doesn't go to gui_interface
3147 return True
3149 def got_connected(self):
3150 ChatControlBase.got_connected(self)
3151 # Refreshing contact
3152 contact = gajim.contacts.get_contact_with_highest_priority(
3153 self.account, self.contact.jid)
3154 if isinstance(contact, GC_Contact):
3155 contact = contact.as_contact()
3156 if contact:
3157 self.contact = contact
3158 self.draw_banner()
3160 def update_status_display(self, name, uf_show, status):
3162 Print the contact's status and update the status/GPG image
3164 self.update_ui()
3165 self.parent_win.redraw_tab(self)
3167 self.print_conversation(_('%(name)s is now %(status)s') % {'name': name,
3168 'status': uf_show}, 'status')
3170 if status:
3171 self.print_conversation(' (', 'status', simple=True)
3172 self.print_conversation('%s' % (status), 'status', simple=True)
3173 self.print_conversation(')', 'status', simple=True)