ability to send messages to a group, even if it contains offline contacts. Fixes...
[gajim.git] / src / chat_control.py
blob548f2ad49c3dd58e4c4ff57d6bf23e70d491dbbb
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
58 from command_system.implementation.middleware import ChatCommandProcessor
59 from command_system.implementation.middleware import CommandTools
60 from command_system.implementation.hosts import ChatCommands
62 # Here we load the module with the standard commands, so they are being detected
63 # and dispatched.
64 import command_system.implementation.standard
66 try:
67 import gtkspell
68 HAS_GTK_SPELL = True
69 except ImportError:
70 HAS_GTK_SPELL = False
72 # the next script, executed in the "po" directory,
73 # generates the following list.
74 ##!/bin/sh
75 #LANG=$(for i in *.po; do j=${i/.po/}; echo -n "_('"$j"')":" '"$j"', " ; done)
76 #echo "{_('en'):'en'",$LANG"}"
77 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'}
79 if gajim.config.get('use_speller') and HAS_GTK_SPELL:
80 # loop removing non-existent dictionaries
81 # iterating on a copy
82 tv = gtk.TextView()
83 spell = gtkspell.Spell(tv)
84 for lang in dict(langs):
85 try:
86 spell.set_language(langs[lang])
87 except OSError:
88 del langs[lang]
89 if spell:
90 spell.detach()
91 del tv
93 ################################################################################
94 class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
95 """
96 A base class containing a banner, ConversationTextview, MessageTextView
97 """
99 keymap = gtk.gdk.keymap_get_default()
100 try:
101 keycode_c = keymap.get_entries_for_keyval(gtk.keysyms.c)[0][0]
102 except TypeError:
103 keycode_c = 54
104 try:
105 keycode_ins = keymap.get_entries_for_keyval(gtk.keysyms.Insert)[0][0]
106 except TypeError:
107 keycode_ins = 118
108 def make_href(self, match):
109 url_color = gajim.config.get('urlmsgcolor')
110 url = match.group()
111 if not '://' in url:
112 url = 'http://' + url
113 return '<a href="%s"><span color="%s">%s</span></a>' % (url,
114 url_color, match.group())
116 def get_font_attrs(self):
118 Get pango font attributes for banner from theme settings
120 theme = gajim.config.get('roster_theme')
121 bannerfont = gajim.config.get_per('themes', theme, 'bannerfont')
122 bannerfontattrs = gajim.config.get_per('themes', theme, 'bannerfontattrs')
124 if bannerfont:
125 font = pango.FontDescription(bannerfont)
126 else:
127 font = pango.FontDescription('Normal')
128 if bannerfontattrs:
129 # B attribute is set by default
130 if 'B' in bannerfontattrs:
131 font.set_weight(pango.WEIGHT_HEAVY)
132 if 'I' in bannerfontattrs:
133 font.set_style(pango.STYLE_ITALIC)
135 font_attrs = 'font_desc="%s"' % font.to_string()
137 # in case there is no font specified we use x-large font size
138 if font.get_size() == 0:
139 font_attrs = '%s size="x-large"' % font_attrs
140 font.set_weight(pango.WEIGHT_NORMAL)
141 font_attrs_small = 'font_desc="%s" size="small"' % font.to_string()
142 return (font_attrs, font_attrs_small)
144 def get_nb_unread(self):
145 jid = self.contact.jid
146 if self.resource:
147 jid += '/' + self.resource
148 type_ = self.type_id
149 return len(gajim.events.get_events(self.account, jid, ['printed_' + type_,
150 type_]))
152 def draw_banner(self):
154 Draw the fat line at the top of the window that houses the icon, jid, etc
156 Derived types MAY implement this.
158 self.draw_banner_text()
159 self._update_banner_state_image()
160 gajim.plugin_manager.gui_extension_point('chat_control_base_draw_banner',
161 self)
163 def update_toolbar(self):
165 update state of buttons in toolbar
167 self._update_toolbar()
168 gajim.plugin_manager.gui_extension_point(
169 'chat_control_base_update_toolbar', self)
171 def draw_banner_text(self):
173 Derived types SHOULD implement this
175 pass
177 def update_ui(self):
179 Derived types SHOULD implement this
181 self.draw_banner()
183 def repaint_themed_widgets(self):
185 Derived types MAY implement this
187 self._paint_banner()
188 self.draw_banner()
190 def _update_banner_state_image(self):
192 Derived types MAY implement this
194 pass
196 def _update_toolbar(self):
198 Derived types MAY implement this
200 pass
202 def _nec_our_status(self, obj):
203 if self.account != obj.conn.name:
204 return
205 if obj.show == 'offline' or (obj.show == 'invisible' and \
206 obj.conn.is_zeroconf):
207 self.got_disconnected()
208 else:
209 # Other code rejoins all GCs, so we don't do it here
210 if not self.type_id == message_control.TYPE_GC:
211 self.got_connected()
212 if self.parent_win:
213 self.parent_win.redraw_tab(self)
215 def _nec_ping_sent(self, obj):
216 if self.contact != obj.contact:
217 return
218 self.print_conversation(_('Ping?'), 'status')
220 def _nec_ping_reply(self, obj):
221 if self.contact != obj.contact:
222 return
223 self.print_conversation(_('Pong! (%s s.)') % obj.seconds, 'status')
225 def _nec_ping_error(self, obj):
226 if self.contact != obj.contact:
227 return
228 self.print_conversation(_('Error.'), 'status')
230 def handle_message_textview_mykey_press(self, widget, event_keyval,
231 event_keymod):
233 Derives types SHOULD implement this, rather than connection to the even
234 itself
236 event = gtk.gdk.Event(gtk.gdk.KEY_PRESS)
237 event.keyval = event_keyval
238 event.state = event_keymod
239 event.time = 0
241 _buffer = widget.get_buffer()
242 start, end = _buffer.get_bounds()
244 if event.keyval -- gtk.keysyms.Tab:
245 position = _buffer.get_insert()
246 end = _buffer.get_iter_at_mark(position)
248 text = _buffer.get_text(start, end, False)
249 text = text.decode('utf8')
251 splitted = text.split()
253 if (text.startswith(self.COMMAND_PREFIX) and not
254 text.startswith(self.COMMAND_PREFIX * 2) and len(splitted) == 1):
256 text = splitted[0]
257 bare = text.lstrip(self.COMMAND_PREFIX)
259 if len(text) == 1:
260 self.command_hits = []
261 for command in self.list_commands():
262 for name in command.names:
263 self.command_hits.append(name)
264 else:
265 if (self.last_key_tabs and self.command_hits and
266 self.command_hits[0].startswith(bare)):
267 self.command_hits.append(self.command_hits.pop(0))
268 else:
269 self.command_hits = []
270 for command in self.list_commands():
271 for name in command.names:
272 if name.startswith(bare):
273 self.command_hits.append(name)
275 if self.command_hits:
276 _buffer.delete(start, end)
277 _buffer.insert_at_cursor(self.COMMAND_PREFIX + self.command_hits[0] + ' ')
278 self.last_key_tabs = True
280 return True
282 self.last_key_tabs = False
284 def status_url_clicked(self, widget, url):
285 helpers.launch_browser_mailer('url', url)
287 def setup_seclabel(self, combo):
288 self.seclabel_combo = combo
289 self.seclabel_combo.hide()
290 self.seclabel_combo.set_no_show_all(True)
291 lb = gtk.ListStore(str)
292 self.seclabel_combo.set_model(lb)
293 cell = gtk.CellRendererText()
294 cell.set_property('xpad', 5) # padding for status text
295 self.seclabel_combo.pack_start(cell, True)
296 # text to show is in in first column of liststore
297 self.seclabel_combo.add_attribute(cell, 'text', 0)
298 if gajim.connections[self.account].seclabel_supported:
299 gajim.connections[self.account].seclabel_catalogue(self.contact.jid, self.on_seclabels_ready)
301 def on_seclabels_ready(self):
302 lb = self.seclabel_combo.get_model()
303 lb.clear()
304 for label in gajim.connections[self.account].seclabel_catalogues[self.contact.jid][2]:
305 lb.append([label])
306 self.seclabel_combo.set_active(0)
307 self.seclabel_combo.set_no_show_all(False)
308 self.seclabel_combo.show_all()
310 def __init__(self, type_id, parent_win, widget_name, contact, acct,
311 resource=None):
312 # Undo needs this variable to know if space has been pressed.
313 # Initialize it to True so empty textview is saved in undo list
314 self.space_pressed = True
316 if resource is None:
317 # We very likely got a contact with a random resource.
318 # This is bad, we need the highest for caps etc.
319 c = gajim.contacts.get_contact_with_highest_priority(
320 acct, contact.jid)
321 if c and not isinstance(c, GC_Contact):
322 contact = c
324 MessageControl.__init__(self, type_id, parent_win, widget_name,
325 contact, acct, resource=resource)
327 widget = self.xml.get_object('history_button')
328 id_ = widget.connect('clicked', self._on_history_menuitem_activate)
329 self.handlers[id_] = widget
331 # when/if we do XHTML we will put formatting buttons back
332 widget = self.xml.get_object('emoticons_button')
333 id_ = widget.connect('clicked', self.on_emoticons_button_clicked)
334 self.handlers[id_] = widget
336 # Create banner and connect signals
337 widget = self.xml.get_object('banner_eventbox')
338 id_ = widget.connect('button-press-event',
339 self._on_banner_eventbox_button_press_event)
340 self.handlers[id_] = widget
342 self.urlfinder = re.compile(
343 r"(www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'\"]+[^!,\.\s<>\)'\"\]]")
345 self.banner_status_label = self.xml.get_object('banner_label')
346 id_ = self.banner_status_label.connect('populate_popup',
347 self.on_banner_label_populate_popup)
348 self.handlers[id_] = self.banner_status_label
350 # Init DND
351 self.TARGET_TYPE_URI_LIST = 80
352 self.dnd_list = [ ( 'text/uri-list', 0, self.TARGET_TYPE_URI_LIST ),
353 ('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_APP, 0)]
354 id_ = self.widget.connect('drag_data_received',
355 self._on_drag_data_received)
356 self.handlers[id_] = self.widget
357 self.widget.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
358 gtk.DEST_DEFAULT_HIGHLIGHT |
359 gtk.DEST_DEFAULT_DROP,
360 self.dnd_list, gtk.gdk.ACTION_COPY)
362 # Create textviews and connect signals
363 self.conv_textview = ConversationTextview(self.account)
364 id_ = self.conv_textview.connect('quote', self.on_quote)
365 self.handlers[id_] = self.conv_textview.tv
366 id_ = self.conv_textview.tv.connect('key_press_event',
367 self._conv_textview_key_press_event)
368 self.handlers[id_] = self.conv_textview.tv
369 # FIXME: DND on non editable TextView, find a better way
370 self.drag_entered = False
371 id_ = self.conv_textview.tv.connect('drag_data_received',
372 self._on_drag_data_received)
373 self.handlers[id_] = self.conv_textview.tv
374 id_ = self.conv_textview.tv.connect('drag_motion', self._on_drag_motion)
375 self.handlers[id_] = self.conv_textview.tv
376 id_ = self.conv_textview.tv.connect('drag_leave', self._on_drag_leave)
377 self.handlers[id_] = self.conv_textview.tv
378 self.conv_textview.tv.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
379 gtk.DEST_DEFAULT_HIGHLIGHT |
380 gtk.DEST_DEFAULT_DROP,
381 self.dnd_list, gtk.gdk.ACTION_COPY)
383 self.conv_scrolledwindow = self.xml.get_object(
384 'conversation_scrolledwindow')
385 self.conv_scrolledwindow.add(self.conv_textview.tv)
386 widget = self.conv_scrolledwindow.get_vadjustment()
387 id_ = widget.connect('value-changed',
388 self.on_conversation_vadjustment_value_changed)
389 self.handlers[id_] = widget
390 id_ = widget.connect('changed',
391 self.on_conversation_vadjustment_changed)
392 self.handlers[id_] = widget
393 self.scroll_to_end_id = None
394 self.was_at_the_end = True
396 # add MessageTextView to UI and connect signals
397 self.msg_scrolledwindow = self.xml.get_object('message_scrolledwindow')
398 self.msg_textview = MessageTextView()
399 id_ = self.msg_textview.connect('mykeypress',
400 self._on_message_textview_mykeypress_event)
401 self.handlers[id_] = self.msg_textview
402 self.msg_scrolledwindow.add(self.msg_textview)
403 id_ = self.msg_textview.connect('key_press_event',
404 self._on_message_textview_key_press_event)
405 self.handlers[id_] = self.msg_textview
406 id_ = self.msg_textview.connect('size-request', self.size_request)
407 self.handlers[id_] = self.msg_textview
408 id_ = self.msg_textview.connect('populate_popup',
409 self.on_msg_textview_populate_popup)
410 self.handlers[id_] = self.msg_textview
411 # Setup DND
412 id_ = self.msg_textview.connect('drag_data_received',
413 self._on_drag_data_received)
414 self.handlers[id_] = self.msg_textview
415 self.msg_textview.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
416 gtk.DEST_DEFAULT_HIGHLIGHT,
417 self.dnd_list, gtk.gdk.ACTION_COPY)
419 self.update_font()
421 # Hook up send button
422 widget = self.xml.get_object('send_button')
423 id_ = widget.connect('clicked', self._on_send_button_clicked)
424 self.handlers[id_] = widget
426 widget = self.xml.get_object('formattings_button')
427 id_ = widget.connect('clicked', self.on_formattings_button_clicked)
428 self.handlers[id_] = widget
430 # the following vars are used to keep history of user's messages
431 self.sent_history = []
432 self.sent_history_pos = 0
433 self.orig_msg = None
435 # Emoticons menu
436 # set image no matter if user wants at this time emoticons or not
437 # (so toggle works ok)
438 img = self.xml.get_object('emoticons_button_image')
439 img.set_from_file(os.path.join(gajim.DATA_DIR, 'emoticons', 'static',
440 'smile.png'))
441 self.toggle_emoticons()
443 # Attach speller
444 if gajim.config.get('use_speller') and HAS_GTK_SPELL:
445 self.set_speller()
446 self.conv_textview.tv.show()
447 self._paint_banner()
449 # For XEP-0172
450 self.user_nick = None
452 self.smooth = True
453 self.msg_textview.grab_focus()
455 self.command_hits = []
456 self.last_key_tabs = False
458 # PluginSystem: adding GUI extension point for ChatControlBase
459 # instance object (also subclasses, eg. ChatControl or GroupchatControl)
460 gajim.plugin_manager.gui_extension_point('chat_control_base', self)
462 gajim.ged.register_event_handler('our-show', ged.GUI1,
463 self._nec_our_status)
464 gajim.ged.register_event_handler('ping-sent', ged.GUI1,
465 self._nec_ping_sent)
466 gajim.ged.register_event_handler('ping-reply', ged.GUI1,
467 self._nec_ping_reply)
468 gajim.ged.register_event_handler('ping-error', ged.GUI1,
469 self._nec_ping_error)
471 # This is bascially a very nasty hack to surpass the inability
472 # to properly use the super, because of the old code.
473 CommandTools.__init__(self)
475 def set_speller(self):
476 # now set the one the user selected
477 per_type = 'contacts'
478 if self.type_id == message_control.TYPE_GC:
479 per_type = 'rooms'
480 lang = gajim.config.get_per(per_type, self.contact.jid,
481 'speller_language')
482 if not lang:
483 # use the default one
484 lang = gajim.config.get('speller_language')
485 if not lang:
486 lang = gajim.LANG
487 if lang:
488 try:
489 gtkspell.Spell(self.msg_textview, lang)
490 self.msg_textview.lang = lang
491 except (gobject.GError, RuntimeError, TypeError, OSError):
492 dialogs.AspellDictError(lang)
494 def on_banner_label_populate_popup(self, label, menu):
496 Override the default context menu and add our own menutiems
498 item = gtk.SeparatorMenuItem()
499 menu.prepend(item)
501 menu2 = self.prepare_context_menu()
502 i = 0
503 for item in menu2:
504 menu2.remove(item)
505 menu.prepend(item)
506 menu.reorder_child(item, i)
507 i += 1
508 menu.show_all()
510 def shutdown(self):
511 # PluginSystem: removing GUI extension points connected with ChatControlBase
512 # instance object
513 gajim.plugin_manager.remove_gui_extension_point('chat_control_base', self)
514 gajim.plugin_manager.remove_gui_extension_point('chat_control_base_draw_banner', self)
515 gajim.ged.remove_event_handler('our-show', ged.GUI1,
516 self._nec_our_status)
518 def on_msg_textview_populate_popup(self, textview, menu):
520 Override the default context menu and we prepend an option to switch
521 languages
523 def _on_select_dictionary(widget, lang):
524 per_type = 'contacts'
525 if self.type_id == message_control.TYPE_GC:
526 per_type = 'rooms'
527 if not gajim.config.get_per(per_type, self.contact.jid):
528 gajim.config.add_per(per_type, self.contact.jid)
529 gajim.config.set_per(per_type, self.contact.jid, 'speller_language',
530 lang)
531 spell = gtkspell.get_from_text_view(self.msg_textview)
532 self.msg_textview.lang = lang
533 spell.set_language(lang)
534 widget.set_active(True)
536 item = gtk.ImageMenuItem(gtk.STOCK_UNDO)
537 menu.prepend(item)
538 id_ = item.connect('activate', self.msg_textview.undo)
539 self.handlers[id_] = item
541 item = gtk.SeparatorMenuItem()
542 menu.prepend(item)
544 item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
545 menu.prepend(item)
546 id_ = item.connect('activate', self.msg_textview.clear)
547 self.handlers[id_] = item
549 if gajim.config.get('use_speller') and HAS_GTK_SPELL:
550 item = gtk.MenuItem(_('Spelling language'))
551 menu.prepend(item)
552 submenu = gtk.Menu()
553 item.set_submenu(submenu)
554 for lang in sorted(langs):
555 item = gtk.CheckMenuItem(lang)
556 if langs[lang] == self.msg_textview.lang:
557 item.set_active(True)
558 submenu.append(item)
559 id_ = item.connect('activate', _on_select_dictionary, langs[lang])
560 self.handlers[id_] = item
562 menu.show_all()
564 def on_quote(self, widget, text):
565 text = '>' + text.replace('\n', '\n>') + '\n'
566 message_buffer = self.msg_textview.get_buffer()
567 message_buffer.insert_at_cursor(text)
569 # moved from ChatControl
570 def _on_banner_eventbox_button_press_event(self, widget, event):
572 If right-clicked, show popup
574 if event.button == 3: # right click
575 self.parent_win.popup_menu(event)
577 def _on_send_button_clicked(self, widget):
579 When send button is pressed: send the current message
581 if gajim.connections[self.account].connected < 2: # we are not connected
582 dialogs.ErrorDialog(_('A connection is not available'),
583 _('Your message can not be sent until you are connected.'))
584 return
585 message_buffer = self.msg_textview.get_buffer()
586 start_iter = message_buffer.get_start_iter()
587 end_iter = message_buffer.get_end_iter()
588 message = message_buffer.get_text(start_iter, end_iter, 0).decode('utf-8')
589 xhtml = self.msg_textview.get_xhtml()
591 # send the message
592 self.send_message(message, xhtml=xhtml)
594 def _paint_banner(self):
596 Repaint banner with theme color
598 theme = gajim.config.get('roster_theme')
599 bgcolor = gajim.config.get_per('themes', theme, 'bannerbgcolor')
600 textcolor = gajim.config.get_per('themes', theme, 'bannertextcolor')
601 # the backgrounds are colored by using an eventbox by
602 # setting the bg color of the eventbox and the fg of the name_label
603 banner_eventbox = self.xml.get_object('banner_eventbox')
604 banner_name_label = self.xml.get_object('banner_name_label')
605 self.disconnect_style_event(banner_name_label)
606 self.disconnect_style_event(self.banner_status_label)
607 if bgcolor:
608 banner_eventbox.modify_bg(gtk.STATE_NORMAL,
609 gtk.gdk.color_parse(bgcolor))
610 default_bg = False
611 else:
612 default_bg = True
613 if textcolor:
614 banner_name_label.modify_fg(gtk.STATE_NORMAL,
615 gtk.gdk.color_parse(textcolor))
616 self.banner_status_label.modify_fg(gtk.STATE_NORMAL,
617 gtk.gdk.color_parse(textcolor))
618 default_fg = False
619 else:
620 default_fg = True
621 if default_bg or default_fg:
622 self._on_style_set_event(banner_name_label, None, default_fg,
623 default_bg)
624 if self.banner_status_label.flags() & gtk.REALIZED:
625 # Widget is realized
626 self._on_style_set_event(self.banner_status_label, None, default_fg,
627 default_bg)
629 def disconnect_style_event(self, widget):
630 # Try to find the event_id
631 for id_ in self.handlers.keys():
632 if self.handlers[id_] == widget:
633 widget.disconnect(id_)
634 del self.handlers[id_]
635 break
637 def connect_style_event(self, widget, set_fg = False, set_bg = False):
638 self.disconnect_style_event(widget)
639 id_ = widget.connect('style-set', self._on_style_set_event, set_fg,
640 set_bg)
641 self.handlers[id_] = widget
643 def _on_style_set_event(self, widget, style, *opts):
645 Set style of widget from style class *.Frame.Eventbox
646 opts[0] == True -> set fg color
647 opts[1] == True -> set bg color
649 banner_eventbox = self.xml.get_object('banner_eventbox')
650 self.disconnect_style_event(widget)
651 if opts[1]:
652 bg_color = widget.style.bg[gtk.STATE_SELECTED]
653 banner_eventbox.modify_bg(gtk.STATE_NORMAL, bg_color)
654 if opts[0]:
655 fg_color = widget.style.fg[gtk.STATE_SELECTED]
656 widget.modify_fg(gtk.STATE_NORMAL, fg_color)
657 self.connect_style_event(widget, opts[0], opts[1])
659 def _conv_textview_key_press_event(self, widget, event):
660 # translate any layout to latin_layout
661 keymap = gtk.gdk.keymap_get_default()
662 keycode = keymap.get_entries_for_keyval(event.keyval)[0][0]
663 if (event.state & gtk.gdk.CONTROL_MASK and keycode in (self.keycode_c,
664 self.keycode_ins)) or (event.state & gtk.gdk.SHIFT_MASK and \
665 event.keyval in (gtk.keysyms.Page_Down, gtk.keysyms.Page_Up)):
666 return False
667 self.parent_win.notebook.emit('key_press_event', event)
668 return True
670 def show_emoticons_menu(self):
671 if not gajim.config.get('emoticons_theme'):
672 return
673 def set_emoticons_menu_position(w, msg_tv = self.msg_textview):
674 window = msg_tv.get_window(gtk.TEXT_WINDOW_WIDGET)
675 # get the window position
676 origin = window.get_origin()
677 size = window.get_size()
678 buf = msg_tv.get_buffer()
679 # get the cursor position
680 cursor = msg_tv.get_iter_location(buf.get_iter_at_mark(
681 buf.get_insert()))
682 cursor = msg_tv.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
683 cursor.x, cursor.y)
684 x = origin[0] + cursor[0]
685 y = origin[1] + size[1]
686 menu_height = gajim.interface.emoticons_menu.size_request()[1]
687 #FIXME: get_line_count is not so good
688 #get the iter of cursor, then tv.get_line_yrange
689 # so we know in which y we are typing (not how many lines we have
690 # then go show just above the current cursor line for up
691 # or just below the current cursor line for down
692 #TEST with having 3 lines and writing in the 2nd
693 if y + menu_height > gtk.gdk.screen_height():
694 # move menu just above cursor
695 y -= menu_height + (msg_tv.allocation.height / buf.get_line_count())
696 #else: # move menu just below cursor
697 # y -= (msg_tv.allocation.height / buf.get_line_count())
698 return (x, y, True) # push_in True
699 gajim.interface.emoticon_menuitem_clicked = self.append_emoticon
700 gajim.interface.emoticons_menu.popup(None, None,
701 set_emoticons_menu_position, 1, 0)
703 def _on_message_textview_key_press_event(self, widget, event):
704 if event.keyval == gtk.keysyms.space:
705 self.space_pressed = True
707 elif (self.space_pressed or self.msg_textview.undo_pressed) and \
708 event.keyval not in (gtk.keysyms.Control_L, gtk.keysyms.Control_R) and \
709 not (event.keyval == gtk.keysyms.z and event.state & gtk.gdk.CONTROL_MASK):
710 # If the space key has been pressed and now it hasnt,
711 # we save the buffer into the undo list. But be carefull we're not
712 # pressiong Control again (as in ctrl+z)
713 _buffer = widget.get_buffer()
714 start_iter, end_iter = _buffer.get_bounds()
715 self.msg_textview.save_undo(_buffer.get_text(start_iter, end_iter))
716 self.space_pressed = False
718 # Ctrl [+ Shift] + Tab are not forwarded to notebook. We handle it here
719 if self.widget_name == 'groupchat_control':
720 if event.keyval not in (gtk.keysyms.ISO_Left_Tab, gtk.keysyms.Tab):
721 self.last_key_tabs = False
722 if event.state & gtk.gdk.SHIFT_MASK:
723 # CTRL + SHIFT + TAB
724 if event.state & gtk.gdk.CONTROL_MASK and \
725 event.keyval == gtk.keysyms.ISO_Left_Tab:
726 self.parent_win.move_to_next_unread_tab(False)
727 return True
728 # SHIFT + PAGE_[UP|DOWN]: send to conv_textview
729 elif event.keyval == gtk.keysyms.Page_Down or \
730 event.keyval == gtk.keysyms.Page_Up:
731 self.conv_textview.tv.emit('key_press_event', event)
732 return True
733 elif event.state & gtk.gdk.CONTROL_MASK:
734 if event.keyval == gtk.keysyms.Tab: # CTRL + TAB
735 self.parent_win.move_to_next_unread_tab(True)
736 return True
737 return False
739 def _on_message_textview_mykeypress_event(self, widget, event_keyval,
740 event_keymod):
742 When a key is pressed: if enter is pressed without the shift key, message
743 (if not empty) is sent and printed in the conversation
745 # NOTE: handles mykeypress which is custom signal connected to this
746 # CB in new_tab(). for this singal see message_textview.py
747 message_textview = widget
748 message_buffer = message_textview.get_buffer()
749 start_iter, end_iter = message_buffer.get_bounds()
750 message = message_buffer.get_text(start_iter, end_iter, False).decode(
751 'utf-8')
752 xhtml = self.msg_textview.get_xhtml()
754 # construct event instance from binding
755 event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) # it's always a key-press here
756 event.keyval = event_keyval
757 event.state = event_keymod
758 event.time = 0 # assign current time
760 if event.keyval == gtk.keysyms.Up:
761 if event.state & gtk.gdk.CONTROL_MASK: # Ctrl+UP
762 self.sent_messages_scroll('up', widget.get_buffer())
763 elif event.keyval == gtk.keysyms.Down:
764 if event.state & gtk.gdk.CONTROL_MASK: # Ctrl+Down
765 self.sent_messages_scroll('down', widget.get_buffer())
766 elif event.keyval == gtk.keysyms.Return or \
767 event.keyval == gtk.keysyms.KP_Enter: # ENTER
768 # NOTE: SHIFT + ENTER is not needed to be emulated as it is not
769 # binding at all (textview's default action is newline)
771 if gajim.config.get('send_on_ctrl_enter'):
772 # here, we emulate GTK default action on ENTER (add new line)
773 # normally I would add in keypress but it gets way to complex
774 # to get instant result on changing this advanced setting
775 if event.state == 0: # no ctrl, no shift just ENTER add newline
776 end_iter = message_buffer.get_end_iter()
777 message_buffer.insert_at_cursor('\n')
778 send_message = False
779 elif event.state & gtk.gdk.CONTROL_MASK: # CTRL + ENTER
780 send_message = True
781 else: # send on Enter, do newline on Ctrl Enter
782 if event.state & gtk.gdk.CONTROL_MASK: # Ctrl + ENTER
783 end_iter = message_buffer.get_end_iter()
784 message_buffer.insert_at_cursor('\n')
785 send_message = False
786 else: # ENTER
787 send_message = True
789 if gajim.connections[self.account].connected < 2 and send_message:
790 # we are not connected
791 dialogs.ErrorDialog(_('A connection is not available'),
792 _('Your message can not be sent until you are connected.'))
793 send_message = False
795 if send_message:
796 self.send_message(message, xhtml=xhtml) # send the message
797 elif event.keyval == gtk.keysyms.z: # CTRL+z
798 if event.state & gtk.gdk.CONTROL_MASK:
799 self.msg_textview.undo()
800 else:
801 # Give the control itself a chance to process
802 self.handle_message_textview_mykey_press(widget, event_keyval,
803 event_keymod)
805 def _on_drag_data_received(self, widget, context, x, y, selection,
806 target_type, timestamp):
808 Derived types SHOULD implement this
810 pass
812 def _on_drag_leave(self, widget, context, time):
813 # FIXME: DND on non editable TextView, find a better way
814 self.drag_entered = False
815 self.conv_textview.tv.set_editable(False)
817 def _on_drag_motion(self, widget, context, x, y, time):
818 # FIXME: DND on non editable TextView, find a better way
819 if not self.drag_entered:
820 # We drag new data over the TextView, make it editable to catch dnd
821 self.drag_entered_conv = True
822 self.conv_textview.tv.set_editable(True)
824 def get_seclabel(self):
825 label = None
826 if self.seclabel_combo is not None:
827 idx = self.seclabel_combo.get_active()
828 if idx != -1:
829 cat = gajim.connections[self.account].seclabel_catalogues[self.contact.jid]
830 lname = cat[2][idx]
831 label = cat[1][lname]
832 return label
834 def send_message(self, message, keyID='', type_='chat', chatstate=None,
835 msg_id=None, composing_xep=None, resource=None, xhtml=None,
836 callback=None, callback_args=[], process_commands=True):
838 Send the given message to the active tab. Doesn't return None if error
840 if not message or message == '\n':
841 return None
843 if process_commands and self.process_as_command(message):
844 return
846 label = self.get_seclabel()
847 MessageControl.send_message(self, message, keyID, type_=type_,
848 chatstate=chatstate, msg_id=msg_id, composing_xep=composing_xep,
849 resource=resource, user_nick=self.user_nick, xhtml=xhtml,
850 label=label,
851 callback=callback, callback_args=callback_args)
853 # Record message history
854 self.save_sent_message(message)
856 # Be sure to send user nickname only once according to JEP-0172
857 self.user_nick = None
859 # Clear msg input
860 message_buffer = self.msg_textview.get_buffer()
861 message_buffer.set_text('') # clear message buffer (and tv of course)
863 def save_sent_message(self, message):
864 # save the message, so user can scroll though the list with key up/down
865 size = len(self.sent_history)
866 # we don't want size of the buffer to grow indefinately
867 max_size = gajim.config.get('key_up_lines')
868 if size >= max_size:
869 for i in xrange(0, size - 1):
870 self.sent_history[i] = self.sent_history[i + 1]
871 self.sent_history[max_size - 1] = message
872 # self.sent_history_pos has changed if we browsed sent_history,
873 # reset to real value
874 self.sent_history_pos = max_size
875 else:
876 self.sent_history.append(message)
877 self.sent_history_pos = size + 1
878 self.orig_msg = None
880 def print_conversation_line(self, text, kind, name, tim,
881 other_tags_for_name=[], other_tags_for_time=[],
882 other_tags_for_text=[], count_as_new=True, subject=None,
883 old_kind=None, xhtml=None, simple=False, xep0184_id=None,
884 graphics=True, displaymarking=None):
886 Print 'chat' type messages
888 jid = self.contact.jid
889 full_jid = self.get_full_jid()
890 textview = self.conv_textview
891 end = False
892 if self.was_at_the_end or kind == 'outgoing':
893 end = True
894 textview.print_conversation_line(text, jid, kind, name, tim,
895 other_tags_for_name, other_tags_for_time, other_tags_for_text,
896 subject, old_kind, xhtml, simple=simple, graphics=graphics,
897 displaymarking=displaymarking)
899 if xep0184_id is not None:
900 textview.show_xep0184_warning(xep0184_id)
902 if not count_as_new:
903 return
904 if kind == 'incoming':
905 if not self.type_id == message_control.TYPE_GC or \
906 gajim.config.get('notify_on_all_muc_messages') or \
907 'marked' in other_tags_for_text:
908 # it's a normal message, or a muc message with want to be
909 # notified about if quitting just after
910 # other_tags_for_text == ['marked'] --> highlighted gc message
911 gajim.last_message_time[self.account][full_jid] = time.time()
913 if kind in ('incoming', 'incoming_queue', 'error'):
914 gc_message = False
915 if self.type_id == message_control.TYPE_GC:
916 gc_message = True
918 if ((self.parent_win and (not self.parent_win.get_active_control() or \
919 self != self.parent_win.get_active_control() or \
920 not self.parent_win.is_active() or not end)) or \
921 (gc_message and \
922 jid in gajim.interface.minimized_controls[self.account])) and \
923 kind in ('incoming', 'incoming_queue', 'error'):
924 # we want to have save this message in events list
925 # other_tags_for_text == ['marked'] --> highlighted gc message
926 if gc_message:
927 if 'marked' in other_tags_for_text:
928 type_ = 'printed_marked_gc_msg'
929 else:
930 type_ = 'printed_gc_msg'
931 event = 'gc_message_received'
932 else:
933 type_ = 'printed_' + self.type_id
934 event = 'message_received'
935 show_in_roster = notify.get_show_in_roster(event,
936 self.account, self.contact, self.session)
937 show_in_systray = notify.get_show_in_systray(event,
938 self.account, self.contact, type_)
940 event = gajim.events.create_event(type_, (self,),
941 show_in_roster = show_in_roster,
942 show_in_systray = show_in_systray)
943 gajim.events.add_event(self.account, full_jid, event)
944 # We need to redraw contact if we show in roster
945 if show_in_roster:
946 gajim.interface.roster.draw_contact(self.contact.jid,
947 self.account)
949 if not self.parent_win:
950 return
952 if (not self.parent_win.get_active_control() or \
953 self != self.parent_win.get_active_control() or \
954 not self.parent_win.is_active() or not end) and \
955 kind in ('incoming', 'incoming_queue', 'error'):
956 self.parent_win.redraw_tab(self)
957 if not self.parent_win.is_active():
958 self.parent_win.show_title(True, self) # Enabled Urgent hint
959 else:
960 self.parent_win.show_title(False, self) # Disabled Urgent hint
962 def toggle_emoticons(self):
964 Hide show emoticons_button and make sure emoticons_menu is always there
965 when needed
967 emoticons_button = self.xml.get_object('emoticons_button')
968 if gajim.config.get('emoticons_theme'):
969 emoticons_button.show()
970 emoticons_button.set_no_show_all(False)
971 else:
972 emoticons_button.hide()
973 emoticons_button.set_no_show_all(True)
975 def append_emoticon(self, str_):
976 buffer_ = self.msg_textview.get_buffer()
977 if buffer_.get_char_count():
978 buffer_.insert_at_cursor(' %s ' % str_)
979 else: # we are the beginning of buffer
980 buffer_.insert_at_cursor('%s ' % str_)
981 self.msg_textview.grab_focus()
983 def on_emoticons_button_clicked(self, widget):
985 Popup emoticons menu
987 gajim.interface.emoticon_menuitem_clicked = self.append_emoticon
988 gajim.interface.popup_emoticons_under_button(widget, self.parent_win)
990 def on_formattings_button_clicked(self, widget):
992 Popup formattings menu
994 menu = gtk.Menu()
996 menuitems = ((_('Bold'), 'bold'),
997 (_('Italic'), 'italic'),
998 (_('Underline'), 'underline'),
999 (_('Strike'), 'strike'))
1001 active_tags = self.msg_textview.get_active_tags()
1003 for menuitem in menuitems:
1004 item = gtk.CheckMenuItem(menuitem[0])
1005 if menuitem[1] in active_tags:
1006 item.set_active(True)
1007 else:
1008 item.set_active(False)
1009 item.connect('activate', self.msg_textview.set_tag,
1010 menuitem[1])
1011 menu.append(item)
1013 item = gtk.SeparatorMenuItem() # separator
1014 menu.append(item)
1016 item = gtk.ImageMenuItem(_('Color'))
1017 icon = gtk.image_new_from_stock(gtk.STOCK_SELECT_COLOR, gtk.ICON_SIZE_MENU)
1018 item.set_image(icon)
1019 item.connect('activate', self.on_color_menuitem_activale)
1020 menu.append(item)
1022 item = gtk.ImageMenuItem(_('Font'))
1023 icon = gtk.image_new_from_stock(gtk.STOCK_SELECT_FONT, gtk.ICON_SIZE_MENU)
1024 item.set_image(icon)
1025 item.connect('activate', self.on_font_menuitem_activale)
1026 menu.append(item)
1028 item = gtk.SeparatorMenuItem() # separator
1029 menu.append(item)
1031 item = gtk.ImageMenuItem(_('Clear formating'))
1032 icon = gtk.image_new_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU)
1033 item.set_image(icon)
1034 item.connect('activate', self.msg_textview.clear_tags)
1035 menu.append(item)
1037 menu.show_all()
1038 gtkgui_helpers.popup_emoticons_under_button(menu, widget,
1039 self.parent_win)
1041 def on_color_menuitem_activale(self, widget):
1042 color_dialog = gtk.ColorSelectionDialog('Select a color')
1043 color_dialog.connect('response', self.msg_textview.color_set,
1044 color_dialog.colorsel)
1045 color_dialog.show_all()
1047 def on_font_menuitem_activale(self, widget):
1048 font_dialog = gtk.FontSelectionDialog('Select a font')
1049 font_dialog.connect('response', self.msg_textview.font_set,
1050 font_dialog.fontsel)
1051 font_dialog.show_all()
1054 def on_actions_button_clicked(self, widget):
1056 Popup action menu
1058 menu = self.prepare_context_menu(hide_buttonbar_items=True)
1059 menu.show_all()
1060 gtkgui_helpers.popup_emoticons_under_button(menu, widget,
1061 self.parent_win)
1063 def update_font(self):
1064 font = pango.FontDescription(gajim.config.get('conversation_font'))
1065 self.conv_textview.tv.modify_font(font)
1066 self.msg_textview.modify_font(font)
1068 def update_tags(self):
1069 self.conv_textview.update_tags()
1071 def clear(self, tv):
1072 buffer_ = tv.get_buffer()
1073 start, end = buffer_.get_bounds()
1074 buffer_.delete(start, end)
1076 def _on_history_menuitem_activate(self, widget = None, jid = None):
1078 When history menuitem is pressed: call history window
1080 if not jid:
1081 jid = self.contact.jid
1083 if 'logs' in gajim.interface.instances:
1084 gajim.interface.instances['logs'].window.present()
1085 gajim.interface.instances['logs'].open_history(jid, self.account)
1086 else:
1087 gajim.interface.instances['logs'] = \
1088 history_window.HistoryWindow(jid, self.account)
1090 def _on_send_file(self, gc_contact=None):
1092 gc_contact can be set when we are in a groupchat control
1094 def _on_ok(c):
1095 gajim.interface.instances['file_transfers'].show_file_send_request(
1096 self.account, c)
1097 if self.TYPE_ID == message_control.TYPE_PM:
1098 gc_contact = self.gc_contact
1099 if gc_contact:
1100 # gc or pm
1101 gc_control = gajim.interface.msg_win_mgr.get_gc_control(
1102 gc_contact.room_jid, self.account)
1103 self_contact = gajim.contacts.get_gc_contact(self.account,
1104 gc_control.room_jid, gc_control.nick)
1105 if gc_control.is_anonymous and gc_contact.affiliation not in ['admin',
1106 'owner'] and self_contact.affiliation in ['admin', 'owner']:
1107 contact = gajim.contacts.get_contact(self.account, gc_contact.jid)
1108 if not contact or contact.sub not in ('both', 'to'):
1109 prim_text = _('Really send file?')
1110 sec_text = _('If you send a file to %s, he/she will know your '
1111 'real Jabber ID.') % gc_contact.name
1112 dialog = dialogs.NonModalConfirmationDialog(prim_text, sec_text,
1113 on_response_ok = (_on_ok, gc_contact))
1114 dialog.popup()
1115 return
1116 _on_ok(gc_contact)
1117 return
1118 _on_ok(self.contact)
1120 def on_minimize_menuitem_toggled(self, widget):
1122 When a grouchat is minimized, unparent the tab, put it in roster etc
1124 old_value = False
1125 minimized_gc = gajim.config.get_per('accounts', self.account,
1126 'minimized_gc').split()
1127 if self.contact.jid in minimized_gc:
1128 old_value = True
1129 minimize = widget.get_active()
1130 if minimize and not self.contact.jid in minimized_gc:
1131 minimized_gc.append(self.contact.jid)
1132 if not minimize and self.contact.jid in minimized_gc:
1133 minimized_gc.remove(self.contact.jid)
1134 if old_value != minimize:
1135 gajim.config.set_per('accounts', self.account, 'minimized_gc',
1136 ' '.join(minimized_gc))
1138 def set_control_active(self, state):
1139 if state:
1140 jid = self.contact.jid
1141 if self.was_at_the_end:
1142 # we are at the end
1143 type_ = ['printed_' + self.type_id]
1144 if self.type_id == message_control.TYPE_GC:
1145 type_ = ['printed_gc_msg', 'printed_marked_gc_msg']
1146 if not gajim.events.remove_events(self.account, self.get_full_jid(),
1147 types = type_):
1148 # There were events to remove
1149 self.redraw_after_event_removed(jid)
1152 def bring_scroll_to_end(self, textview, diff_y = 0):
1154 Scroll to the end of textview if end is not visible
1156 if self.scroll_to_end_id:
1157 # a scroll is already planned
1158 return
1159 buffer_ = textview.get_buffer()
1160 end_iter = buffer_.get_end_iter()
1161 end_rect = textview.get_iter_location(end_iter)
1162 visible_rect = textview.get_visible_rect()
1163 # scroll only if expected end is not visible
1164 if end_rect.y >= (visible_rect.y + visible_rect.height + diff_y):
1165 self.scroll_to_end_id = gobject.idle_add(self.scroll_to_end_iter,
1166 textview)
1168 def scroll_to_end_iter(self, textview):
1169 buffer_ = textview.get_buffer()
1170 end_iter = buffer_.get_end_iter()
1171 textview.scroll_to_iter(end_iter, 0, False, 1, 1)
1172 self.scroll_to_end_id = None
1173 return False
1175 def size_request(self, msg_textview, requisition):
1177 When message_textview changes its size: if the new height will enlarge
1178 the window, enable the scrollbar automatic policy. Also enable scrollbar
1179 automatic policy for horizontal scrollbar if message we have in
1180 message_textview is too big
1182 if msg_textview.window is None:
1183 return
1185 min_height = self.conv_scrolledwindow.get_property('height-request')
1186 conversation_height = self.conv_textview.tv.window.get_size()[1]
1187 message_height = msg_textview.window.get_size()[1]
1188 message_width = msg_textview.window.get_size()[0]
1189 # new tab is not exposed yet
1190 if conversation_height < 2:
1191 return
1193 if conversation_height < min_height:
1194 min_height = conversation_height
1196 # we don't want to always resize in height the message_textview
1197 # so we have minimum on conversation_textview's scrolled window
1198 # but we also want to avoid window resizing so if we reach that
1199 # minimum for conversation_textview and maximum for message_textview
1200 # we set to automatic the scrollbar policy
1201 diff_y = message_height - requisition.height
1202 if diff_y != 0:
1203 if conversation_height + diff_y < min_height:
1204 if message_height + conversation_height - min_height > min_height:
1205 policy = self.msg_scrolledwindow.get_property(
1206 'vscrollbar-policy')
1207 # scroll only when scrollbar appear
1208 if policy != gtk.POLICY_AUTOMATIC:
1209 self.msg_scrolledwindow.set_property('vscrollbar-policy',
1210 gtk.POLICY_AUTOMATIC)
1211 self.msg_scrolledwindow.set_property('height-request',
1212 message_height + conversation_height - min_height)
1213 self.bring_scroll_to_end(msg_textview)
1214 else:
1215 self.msg_scrolledwindow.set_property('vscrollbar-policy',
1216 gtk.POLICY_NEVER)
1217 self.msg_scrolledwindow.set_property('height-request', -1)
1218 self.conv_textview.bring_scroll_to_end(diff_y - 18, False)
1219 else:
1220 self.conv_textview.bring_scroll_to_end(diff_y - 18, self.smooth)
1221 self.smooth = True # reinit the flag
1222 # enable scrollbar automatic policy for horizontal scrollbar
1223 # if message we have in message_textview is too big
1224 if requisition.width > message_width:
1225 self.msg_scrolledwindow.set_property('hscrollbar-policy',
1226 gtk.POLICY_AUTOMATIC)
1227 else:
1228 self.msg_scrolledwindow.set_property('hscrollbar-policy',
1229 gtk.POLICY_NEVER)
1231 return True
1233 def on_conversation_vadjustment_changed(self, adjustment):
1234 # used to stay at the end of the textview when we shrink conversation
1235 # textview.
1236 if self.was_at_the_end:
1237 self.conv_textview.bring_scroll_to_end(-18)
1238 self.was_at_the_end = (adjustment.upper - adjustment.value - adjustment.page_size) < 18
1240 def on_conversation_vadjustment_value_changed(self, adjustment):
1241 # stop automatic scroll when we manually scroll
1242 if not self.conv_textview.auto_scrolling:
1243 self.conv_textview.stop_scrolling()
1244 self.was_at_the_end = (adjustment.upper - adjustment.value - adjustment.page_size) < 18
1245 if self.resource:
1246 jid = self.contact.get_full_jid()
1247 else:
1248 jid = self.contact.jid
1249 types_list = []
1250 type_ = self.type_id
1251 if type_ == message_control.TYPE_GC:
1252 type_ = 'gc_msg'
1253 types_list = ['printed_' + type_, type_, 'printed_marked_gc_msg']
1254 else: # Not a GC
1255 types_list = ['printed_' + type_, type_]
1257 if not len(gajim.events.get_events(self.account, jid, types_list)):
1258 return
1259 if not self.parent_win:
1260 return
1261 if self.conv_textview.at_the_end() and \
1262 self.parent_win.get_active_control() == self and \
1263 self.parent_win.window.is_active():
1264 # we are at the end
1265 if self.type_id == message_control.TYPE_GC:
1266 if not gajim.events.remove_events(self.account, jid,
1267 types=types_list):
1268 self.redraw_after_event_removed(jid)
1269 elif self.session and self.session.remove_events(types_list):
1270 # There were events to remove
1271 self.redraw_after_event_removed(jid)
1273 def redraw_after_event_removed(self, jid):
1275 We just removed a 'printed_*' event, redraw contact in roster or
1276 gc_roster and titles in roster and msg_win
1278 self.parent_win.redraw_tab(self)
1279 self.parent_win.show_title()
1280 # TODO : get the contact and check notify.get_show_in_roster()
1281 if self.type_id == message_control.TYPE_PM:
1282 room_jid, nick = gajim.get_room_and_nick_from_fjid(jid)
1283 groupchat_control = gajim.interface.msg_win_mgr.get_gc_control(
1284 room_jid, self.account)
1285 if room_jid in gajim.interface.minimized_controls[self.account]:
1286 groupchat_control = \
1287 gajim.interface.minimized_controls[self.account][room_jid]
1288 contact = \
1289 gajim.contacts.get_contact_with_highest_priority(self.account, \
1290 room_jid)
1291 if contact:
1292 gajim.interface.roster.draw_contact(room_jid, self.account)
1293 if groupchat_control:
1294 groupchat_control.draw_contact(nick)
1295 if groupchat_control.parent_win:
1296 groupchat_control.parent_win.redraw_tab(groupchat_control)
1297 else:
1298 gajim.interface.roster.draw_contact(jid, self.account)
1299 gajim.interface.roster.show_title()
1301 def sent_messages_scroll(self, direction, conv_buf):
1302 size = len(self.sent_history)
1303 if self.orig_msg is None:
1304 # user was typing something and then went into history, so save
1305 # whatever is already typed
1306 start_iter = conv_buf.get_start_iter()
1307 end_iter = conv_buf.get_end_iter()
1308 self.orig_msg = conv_buf.get_text(start_iter, end_iter, 0).decode(
1309 'utf-8')
1310 if direction == 'up':
1311 if self.sent_history_pos == 0:
1312 return
1313 self.sent_history_pos = self.sent_history_pos - 1
1314 self.smooth = False
1315 conv_buf.set_text(self.sent_history[self.sent_history_pos])
1316 elif direction == 'down':
1317 if self.sent_history_pos >= size - 1:
1318 conv_buf.set_text(self.orig_msg)
1319 self.orig_msg = None
1320 self.sent_history_pos = size
1321 return
1323 self.sent_history_pos = self.sent_history_pos + 1
1324 self.smooth = False
1325 conv_buf.set_text(self.sent_history[self.sent_history_pos])
1327 def lighten_color(self, color):
1328 p = 0.4
1329 mask = 0
1330 color.red = int((color.red * p) + (mask * (1 - p)))
1331 color.green = int((color.green * p) + (mask * (1 - p)))
1332 color.blue = int((color.blue * p) + (mask * (1 - p)))
1333 return color
1335 def widget_set_visible(self, widget, state):
1337 Show or hide a widget
1339 # make the last message visible, when changing to "full view"
1340 if not state:
1341 gobject.idle_add(self.conv_textview.scroll_to_end_iter)
1343 widget.set_no_show_all(state)
1344 if state:
1345 widget.hide()
1346 else:
1347 widget.show_all()
1349 def chat_buttons_set_visible(self, state):
1351 Toggle chat buttons
1353 MessageControl.chat_buttons_set_visible(self, state)
1354 self.widget_set_visible(self.xml.get_object('actions_hbox'), state)
1356 def got_connected(self):
1357 self.msg_textview.set_sensitive(True)
1358 self.msg_textview.set_editable(True)
1359 # FIXME: Set sensitivity for toolbar
1361 def got_disconnected(self):
1362 self.msg_textview.set_sensitive(False)
1363 self.msg_textview.set_editable(False)
1364 self.conv_textview.tv.grab_focus()
1366 self.no_autonegotiation = False
1367 # FIXME: Set sensitivity for toolbar
1369 ################################################################################
1370 class ChatControl(ChatControlBase):
1372 A control for standard 1-1 chat
1375 JINGLE_STATE_NULL,
1376 JINGLE_STATE_CONNECTING,
1377 JINGLE_STATE_CONNECTION_RECEIVED,
1378 JINGLE_STATE_CONNECTED,
1379 JINGLE_STATE_ERROR
1380 ) = range(5)
1382 TYPE_ID = message_control.TYPE_CHAT
1383 old_msg_kind = None # last kind of the printed message
1385 # Set a command host to bound to. Every command given through a chat will be
1386 # processed with this command host.
1387 COMMAND_HOST = ChatCommands
1389 def __init__(self, parent_win, contact, acct, session, resource = None):
1390 ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
1391 'chat_control', contact, acct, resource)
1393 self.gpg_is_active = False
1394 # for muc use:
1395 # widget = self.xml.get_object('muc_window_actions_button')
1396 self.actions_button = self.xml.get_object('message_window_actions_button')
1397 id_ = self.actions_button.connect('clicked',
1398 self.on_actions_button_clicked)
1399 self.handlers[id_] = self.actions_button
1401 self._formattings_button = self.xml.get_object('formattings_button')
1403 self._add_to_roster_button = self.xml.get_object(
1404 'add_to_roster_button')
1405 id_ = self._add_to_roster_button.connect('clicked',
1406 self._on_add_to_roster_menuitem_activate)
1407 self.handlers[id_] = self._add_to_roster_button
1409 self._audio_button = self.xml.get_object('audio_togglebutton')
1410 id_ = self._audio_button.connect('toggled', self.on_audio_button_toggled)
1411 self.handlers[id_] = self._audio_button
1412 # add a special img
1413 gtkgui_helpers.add_image_to_button(self._audio_button,
1414 'gajim-mic_inactive')
1416 self._video_button = self.xml.get_object('video_togglebutton')
1417 id_ = self._video_button.connect('toggled', self.on_video_button_toggled)
1418 self.handlers[id_] = self._video_button
1419 # add a special img
1420 gtkgui_helpers.add_image_to_button(self._video_button,
1421 'gajim-cam_inactive')
1423 self._send_file_button = self.xml.get_object('send_file_button')
1424 # add a special img for send file button
1425 path_to_upload_img = gtkgui_helpers.get_icon_path('gajim-upload')
1426 img = gtk.Image()
1427 img.set_from_file(path_to_upload_img)
1428 self._send_file_button.set_image(img)
1429 id_ = self._send_file_button.connect('clicked',
1430 self._on_send_file_menuitem_activate)
1431 self.handlers[id_] = self._send_file_button
1433 self._convert_to_gc_button = self.xml.get_object(
1434 'convert_to_gc_button')
1435 id_ = self._convert_to_gc_button.connect('clicked',
1436 self._on_convert_to_gc_menuitem_activate)
1437 self.handlers[id_] = self._convert_to_gc_button
1439 contact_information_button = self.xml.get_object(
1440 'contact_information_button')
1441 id_ = contact_information_button.connect('clicked',
1442 self._on_contact_information_menuitem_activate)
1443 self.handlers[id_] = contact_information_button
1445 compact_view = gajim.config.get('compact_view')
1446 self.chat_buttons_set_visible(compact_view)
1447 self.widget_set_visible(self.xml.get_object('banner_eventbox'),
1448 gajim.config.get('hide_chat_banner'))
1450 self.authentication_button = self.xml.get_object(
1451 'authentication_button')
1452 id_ = self.authentication_button.connect('clicked',
1453 self._on_authentication_button_clicked)
1454 self.handlers[id_] = self.authentication_button
1456 # Add lock image to show chat encryption
1457 self.lock_image = self.xml.get_object('lock_image')
1459 # Convert to GC icon
1460 img = self.xml.get_object('convert_to_gc_button_image')
1461 img.set_from_pixbuf(gtkgui_helpers.load_icon(
1462 'muc_active').get_pixbuf())
1464 self._audio_banner_image = self.xml.get_object('audio_banner_image')
1465 self._video_banner_image = self.xml.get_object('video_banner_image')
1466 self.audio_sid = None
1467 self.audio_state = self.JINGLE_STATE_NULL
1468 self.audio_available = False
1469 self.video_sid = None
1470 self.video_state = self.JINGLE_STATE_NULL
1471 self.video_available = False
1473 self.update_toolbar()
1475 self._pep_images = {}
1476 self._pep_images['mood'] = self.xml.get_object('mood_image')
1477 self._pep_images['activity'] = self.xml.get_object('activity_image')
1478 self._pep_images['tune'] = self.xml.get_object('tune_image')
1479 self._pep_images['location'] = self.xml.get_object('location_image')
1480 self.update_all_pep_types()
1482 # keep timeout id and window obj for possible big avatar
1483 # it is on enter-notify and leave-notify so no need to be
1484 # per jid
1485 self.show_bigger_avatar_timeout_id = None
1486 self.bigger_avatar_window = None
1487 self.show_avatar()
1489 # chatstate timers and state
1490 self.reset_kbd_mouse_timeout_vars()
1491 self._schedule_activity_timers()
1493 # Hook up signals
1494 id_ = self.parent_win.window.connect('motion-notify-event',
1495 self._on_window_motion_notify)
1496 self.handlers[id_] = self.parent_win.window
1497 message_tv_buffer = self.msg_textview.get_buffer()
1498 id_ = message_tv_buffer.connect('changed',
1499 self._on_message_tv_buffer_changed)
1500 self.handlers[id_] = message_tv_buffer
1502 widget = self.xml.get_object('avatar_eventbox')
1503 widget.set_property('height-request', gajim.config.get(
1504 'chat_avatar_height'))
1505 id_ = widget.connect('enter-notify-event',
1506 self.on_avatar_eventbox_enter_notify_event)
1507 self.handlers[id_] = widget
1509 id_ = widget.connect('leave-notify-event',
1510 self.on_avatar_eventbox_leave_notify_event)
1511 self.handlers[id_] = widget
1513 id_ = widget.connect('button-press-event',
1514 self.on_avatar_eventbox_button_press_event)
1515 self.handlers[id_] = widget
1517 widget = self.xml.get_object('location_eventbox')
1518 id_ = widget.connect('button-release-event',
1519 self.on_location_eventbox_button_release_event)
1520 self.handlers[id_] = widget
1522 for key in ('1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'):
1523 widget = self.xml.get_object(key + '_button')
1524 id_ = widget.connect('pressed', self.on_num_button_pressed, key)
1525 self.handlers[id_] = widget
1526 id_ = widget.connect('released', self.on_num_button_released)
1527 self.handlers[id_] = widget
1529 self.dtmf_window = self.xml.get_object('dtmf_window')
1530 id_ = self.dtmf_window.connect('focus-out-event',
1531 self.on_dtmf_window_focus_out_event)
1532 self.handlers[id_] = self.dtmf_window
1534 widget = self.xml.get_object('dtmf_button')
1535 id_ = widget.connect('clicked', self.on_dtmf_button_clicked)
1536 self.handlers[id_] = widget
1538 widget = self.xml.get_object('mic_hscale')
1539 id_ = widget.connect('value_changed', self.on_mic_hscale_value_changed)
1540 self.handlers[id_] = widget
1542 widget = self.xml.get_object('sound_hscale')
1543 id_ = widget.connect('value_changed', self.on_sound_hscale_value_changed)
1544 self.handlers[id_] = widget
1546 if not session:
1547 # Don't use previous session if we want to a specific resource
1548 # and it's not the same
1549 if not resource:
1550 resource = contact.resource
1551 session = gajim.connections[self.account].find_controlless_session(
1552 self.contact.jid, resource)
1554 self.setup_seclabel(self.xml.get_object('label_selector'))
1555 if session:
1556 session.control = self
1557 self.session = session
1559 if session.enable_encryption:
1560 self.print_esession_details()
1562 # Enable encryption if needed
1563 self.no_autonegotiation = False
1564 e2e_is_active = self.session and self.session.enable_encryption
1565 gpg_pref = gajim.config.get_per('contacts', contact.jid,
1566 'gpg_enabled')
1568 # try GPG first
1569 if not e2e_is_active and gpg_pref and \
1570 gajim.config.get_per('accounts', self.account, 'keyid') and \
1571 gajim.connections[self.account].USE_GPG:
1572 self.gpg_is_active = True
1573 gajim.encrypted_chats[self.account].append(contact.jid)
1574 msg = _('GPG encryption enabled')
1575 ChatControlBase.print_conversation_line(self, msg,
1576 'status', '', None)
1578 if self.session:
1579 self.session.loggable = gajim.config.get_per('accounts',
1580 self.account, 'log_encrypted_sessions')
1581 # GPG is always authenticated as we use GPG's WoT
1582 self._show_lock_image(self.gpg_is_active, 'GPG', self.gpg_is_active,
1583 self.session and self.session.is_loggable(), True)
1585 self.update_ui()
1586 # restore previous conversation
1587 self.restore_conversation()
1588 self.msg_textview.grab_focus()
1590 # change tooltip text for audio and video buttons if python-farsight is
1591 # not installed
1592 if not gajim.HAVE_FARSIGHT:
1593 tooltip_text = self._audio_button.get_tooltip_text()
1594 self._audio_button.set_tooltip_text(
1595 '%s\n%s' % (tooltip_text, _('Requires python-farsight.')))
1596 tooltip_text = self._video_button.get_tooltip_text()
1597 self._video_button.set_tooltip_text(
1598 '%s\n%s' % (tooltip_text, _('Requires python-farsight.')))
1600 gajim.ged.register_event_handler('pep-received', ged.GUI1,
1601 self._nec_pep_received)
1602 gajim.ged.register_event_handler('vcard-received', ged.GUI1,
1603 self._nec_vcard_received)
1604 gajim.ged.register_event_handler('failed-decrypt', ged.GUI1,
1605 self._nec_failed_decrypt)
1606 gajim.ged.register_event_handler('chatstate-received', ged.GUI1,
1607 self._nec_chatstate_received)
1608 gajim.ged.register_event_handler('caps-received', ged.GUI1,
1609 self._nec_caps_received)
1611 # PluginSystem: adding GUI extension point for this ChatControl
1612 # instance object
1613 gajim.plugin_manager.gui_extension_point('chat_control', self)
1615 def _update_toolbar(self):
1616 # Formatting
1617 if self.contact.supports(NS_XHTML_IM) and not self.gpg_is_active:
1618 self._formattings_button.set_sensitive(True)
1619 else:
1620 self._formattings_button.set_sensitive(False)
1622 # Add to roster
1623 if not isinstance(self.contact, GC_Contact) \
1624 and _('Not in Roster') in self.contact.groups:
1625 self._add_to_roster_button.show()
1626 else:
1627 self._add_to_roster_button.hide()
1629 # Jingle detection
1630 if self.contact.supports(NS_JINGLE_ICE_UDP) and \
1631 gajim.HAVE_FARSIGHT and self.contact.resource:
1632 self.audio_available = self.contact.supports(NS_JINGLE_RTP_AUDIO)
1633 self.video_available = self.contact.supports(NS_JINGLE_RTP_VIDEO)
1634 else:
1635 if self.video_available or self.audio_available:
1636 self.stop_jingle()
1637 self.video_available = False
1638 self.audio_available = False
1640 # Audio buttons
1641 self._audio_button.set_sensitive(self.audio_available)
1643 # Video buttons
1644 self._video_button.set_sensitive(self.video_available)
1646 # Send file
1647 if self.contact.supports(NS_FILE) and self.contact.resource:
1648 self._send_file_button.set_sensitive(True)
1649 self._send_file_button.set_tooltip_text('')
1650 else:
1651 self._send_file_button.set_sensitive(False)
1652 if not self.contact.supports(NS_FILE):
1653 self._send_file_button.set_tooltip_text(_(
1654 "This contact does not support file transfer."))
1655 else:
1656 self._send_file_button.set_tooltip_text(
1657 _("You need to know the real JID of the contact to send him or "
1658 "her a file."))
1660 # Convert to GC
1661 if self.contact.supports(NS_MUC):
1662 self._convert_to_gc_button.set_sensitive(True)
1663 else:
1664 self._convert_to_gc_button.set_sensitive(False)
1666 def update_all_pep_types(self):
1667 for pep_type in self._pep_images:
1668 self.update_pep(pep_type)
1670 def update_pep(self, pep_type):
1671 if isinstance(self.contact, GC_Contact):
1672 return
1673 if pep_type not in self._pep_images:
1674 return
1675 pep = self.contact.pep
1676 img = self._pep_images[pep_type]
1677 if pep_type in pep:
1678 img.set_from_pixbuf(pep[pep_type].asPixbufIcon())
1679 img.set_tooltip_markup(pep[pep_type].asMarkupText())
1680 img.show()
1681 else:
1682 img.hide()
1684 def _nec_pep_received(self, obj):
1685 if obj.conn.name != self.account:
1686 return
1687 if obj.jid != self.contact.jid:
1688 return
1690 if obj.pep_type == 'nickname':
1691 self.update_ui()
1692 self.parent_win.redraw_tab(self)
1693 self.parent_win.show_title()
1694 else:
1695 self.update_pep(obj.pep_type)
1697 def _update_jingle(self, jingle_type):
1698 if jingle_type not in ('audio', 'video'):
1699 return
1700 banner_image = getattr(self, '_' + jingle_type + '_banner_image')
1701 state = getattr(self, jingle_type + '_state')
1702 if state == self.JINGLE_STATE_NULL:
1703 banner_image.hide()
1704 else:
1705 banner_image.show()
1706 if state == self.JINGLE_STATE_CONNECTING:
1707 banner_image.set_from_stock(
1708 gtk.STOCK_CONVERT, 1)
1709 elif state == self.JINGLE_STATE_CONNECTION_RECEIVED:
1710 banner_image.set_from_stock(
1711 gtk.STOCK_NETWORK, 1)
1712 elif state == self.JINGLE_STATE_CONNECTED:
1713 banner_image.set_from_stock(
1714 gtk.STOCK_CONNECT, 1)
1715 elif state == self.JINGLE_STATE_ERROR:
1716 banner_image.set_from_stock(
1717 gtk.STOCK_DIALOG_WARNING, 1)
1718 self.update_toolbar()
1720 def update_audio(self):
1721 self._update_jingle('audio')
1722 hbox = self.xml.get_object('audio_buttons_hbox')
1723 if self.audio_state == self.JINGLE_STATE_CONNECTED:
1724 # Set volume from config
1725 input_vol = gajim.config.get('audio_input_volume')
1726 output_vol = gajim.config.get('audio_output_volume')
1727 input_vol = max(min(input_vol, 100), 0)
1728 output_vol = max(min(output_vol, 100), 0)
1729 self.xml.get_object('mic_hscale').set_value(input_vol)
1730 self.xml.get_object('sound_hscale').set_value(output_vol)
1731 # Show vbox
1732 hbox.set_no_show_all(False)
1733 hbox.show_all()
1734 elif not self.audio_sid:
1735 hbox.set_no_show_all(True)
1736 hbox.hide()
1738 def update_video(self):
1739 self._update_jingle('video')
1741 def change_resource(self, resource):
1742 old_full_jid = self.get_full_jid()
1743 self.resource = resource
1744 new_full_jid = self.get_full_jid()
1745 # update gajim.last_message_time
1746 if old_full_jid in gajim.last_message_time[self.account]:
1747 gajim.last_message_time[self.account][new_full_jid] = \
1748 gajim.last_message_time[self.account][old_full_jid]
1749 # update events
1750 gajim.events.change_jid(self.account, old_full_jid, new_full_jid)
1751 # update MessageWindow._controls
1752 self.parent_win.change_jid(self.account, old_full_jid, new_full_jid)
1754 def stop_jingle(self, sid=None, reason=None):
1755 if self.audio_sid and sid in (self.audio_sid, None):
1756 self.close_jingle_content('audio')
1757 if self.video_sid and sid in (self.video_sid, None):
1758 self.close_jingle_content('video')
1761 def _set_jingle_state(self, jingle_type, state, sid=None, reason=None):
1762 if jingle_type not in ('audio', 'video'):
1763 return
1764 if state in ('connecting', 'connected', 'stop', 'error') and reason:
1765 str = _('%(type)s state : %(state)s, reason: %(reason)s') % {
1766 'type': jingle_type.capitalize(), 'state': state, 'reason': reason}
1767 self.print_conversation(str, 'info')
1769 states = {'connecting': self.JINGLE_STATE_CONNECTING,
1770 'connection_received': self.JINGLE_STATE_CONNECTION_RECEIVED,
1771 'connected': self.JINGLE_STATE_CONNECTED,
1772 'stop': self.JINGLE_STATE_NULL,
1773 'error': self.JINGLE_STATE_ERROR}
1775 jingle_state = states[state]
1776 if getattr(self, jingle_type + '_state') == jingle_state or state == 'error':
1777 return
1779 if state == 'stop' and getattr(self, jingle_type + '_sid') not in (None, sid):
1780 return
1782 setattr(self, jingle_type + '_state', jingle_state)
1784 if jingle_state == self.JINGLE_STATE_NULL:
1785 setattr(self, jingle_type + '_sid', None)
1786 if state in ('connection_received', 'connecting'):
1787 setattr(self, jingle_type + '_sid', sid)
1789 getattr(self, '_' + jingle_type + '_button').set_active(jingle_state != self.JINGLE_STATE_NULL)
1791 getattr(self, 'update_' + jingle_type)()
1793 def set_audio_state(self, state, sid=None, reason=None):
1794 self._set_jingle_state('audio', state, sid=sid, reason=reason)
1796 def set_video_state(self, state, sid=None, reason=None):
1797 self._set_jingle_state('video', state, sid=sid, reason=reason)
1799 def _get_audio_content(self):
1800 session = gajim.connections[self.account].get_jingle_session(
1801 self.contact.get_full_jid(), self.audio_sid)
1802 return session.get_content('audio')
1804 def on_num_button_pressed(self, widget, num):
1805 self._get_audio_content()._start_dtmf(num)
1807 def on_num_button_released(self, released):
1808 self._get_audio_content()._stop_dtmf()
1810 def on_dtmf_button_clicked(self, widget):
1811 self.dtmf_window.show_all()
1813 def on_dtmf_window_focus_out_event(self, widget, event):
1814 self.dtmf_window.hide()
1816 def on_mic_hscale_value_changed(self, widget, value):
1817 self._get_audio_content().set_mic_volume(value / 100)
1818 # Save volume to config
1819 gajim.config.set('audio_input_volume', value)
1822 def on_sound_hscale_value_changed(self, widget, value):
1823 self._get_audio_content().set_out_volume(value / 100)
1824 # Save volume to config
1825 gajim.config.set('audio_output_volume', value)
1827 def on_avatar_eventbox_enter_notify_event(self, widget, event):
1829 Enter the eventbox area so we under conditions add a timeout to show a
1830 bigger avatar after 0.5 sec
1832 jid = self.contact.jid
1833 avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid)
1834 if avatar_pixbuf in ('ask', None):
1835 return
1836 avatar_w = avatar_pixbuf.get_width()
1837 avatar_h = avatar_pixbuf.get_height()
1839 scaled_buf = self.xml.get_object('avatar_image').get_pixbuf()
1840 scaled_buf_w = scaled_buf.get_width()
1841 scaled_buf_h = scaled_buf.get_height()
1843 # do we have something bigger to show?
1844 if avatar_w > scaled_buf_w or avatar_h > scaled_buf_h:
1845 # wait for 0.5 sec in case we leave earlier
1846 if self.show_bigger_avatar_timeout_id is not None:
1847 gobject.source_remove(self.show_bigger_avatar_timeout_id)
1848 self.show_bigger_avatar_timeout_id = gobject.timeout_add(500,
1849 self.show_bigger_avatar, widget)
1851 def on_avatar_eventbox_leave_notify_event(self, widget, event):
1853 Left the eventbox area that holds the avatar img
1855 # did we add a timeout? if yes remove it
1856 if self.show_bigger_avatar_timeout_id is not None:
1857 gobject.source_remove(self.show_bigger_avatar_timeout_id)
1858 self.show_bigger_avatar_timeout_id = None
1860 def on_avatar_eventbox_button_press_event(self, widget, event):
1862 If right-clicked, show popup
1864 if event.button == 3: # right click
1865 menu = gtk.Menu()
1866 menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS)
1867 id_ = menuitem.connect('activate',
1868 gtkgui_helpers.on_avatar_save_as_menuitem_activate,
1869 self.contact.jid, self.contact.get_shown_name())
1870 self.handlers[id_] = menuitem
1871 menu.append(menuitem)
1872 menu.show_all()
1873 menu.connect('selection-done', lambda w:w.destroy())
1874 # show the menu
1875 menu.show_all()
1876 menu.popup(None, None, None, event.button, event.time)
1877 return True
1879 def on_location_eventbox_button_release_event(self, widget, event):
1880 if 'location' in self.contact.pep:
1881 location = self.contact.pep['location']._pep_specific_data
1882 if ('lat' in location) and ('lon' in location):
1883 uri = 'http://www.openstreetmap.org/?' + \
1884 'mlat=%(lat)s&mlon=%(lon)s&zoom=16' % {'lat': location['lat'],
1885 'lon': location['lon']}
1886 helpers.launch_browser_mailer('url', uri)
1888 def _on_window_motion_notify(self, widget, event):
1890 It gets called no matter if it is the active window or not
1892 if self.parent_win.get_active_jid() == self.contact.jid:
1893 # if window is the active one, change vars assisting chatstate
1894 self.mouse_over_in_last_5_secs = True
1895 self.mouse_over_in_last_30_secs = True
1897 def _schedule_activity_timers(self):
1898 self.possible_paused_timeout_id = gobject.timeout_add_seconds(5,
1899 self.check_for_possible_paused_chatstate, None)
1900 self.possible_inactive_timeout_id = gobject.timeout_add_seconds(30,
1901 self.check_for_possible_inactive_chatstate, None)
1903 def update_ui(self):
1904 # The name banner is drawn here
1905 ChatControlBase.update_ui(self)
1906 self.update_toolbar()
1908 def _update_banner_state_image(self):
1909 contact = gajim.contacts.get_contact_with_highest_priority(self.account,
1910 self.contact.jid)
1911 if not contact or self.resource:
1912 # For transient contacts
1913 contact = self.contact
1914 show = contact.show
1915 jid = contact.jid
1917 # Set banner image
1918 img_32 = gajim.interface.roster.get_appropriate_state_images(jid,
1919 size = '32', icon_name = show)
1920 img_16 = gajim.interface.roster.get_appropriate_state_images(jid,
1921 icon_name = show)
1922 if show in img_32 and img_32[show].get_pixbuf():
1923 # we have 32x32! use it!
1924 banner_image = img_32[show]
1925 use_size_32 = True
1926 else:
1927 banner_image = img_16[show]
1928 use_size_32 = False
1930 banner_status_img = self.xml.get_object('banner_status_image')
1931 if banner_image.get_storage_type() == gtk.IMAGE_ANIMATION:
1932 banner_status_img.set_from_animation(banner_image.get_animation())
1933 else:
1934 pix = banner_image.get_pixbuf()
1935 if pix is not None:
1936 if use_size_32:
1937 banner_status_img.set_from_pixbuf(pix)
1938 else: # we need to scale 16x16 to 32x32
1939 scaled_pix = pix.scale_simple(32, 32,
1940 gtk.gdk.INTERP_BILINEAR)
1941 banner_status_img.set_from_pixbuf(scaled_pix)
1943 def draw_banner_text(self):
1945 Draw the text in the fat line at the top of the window that houses the
1946 name, jid
1948 contact = self.contact
1949 jid = contact.jid
1951 banner_name_label = self.xml.get_object('banner_name_label')
1953 name = contact.get_shown_name()
1954 if self.resource:
1955 name += '/' + self.resource
1956 if self.TYPE_ID == message_control.TYPE_PM:
1957 name = _('%(nickname)s from group chat %(room_name)s') %\
1958 {'nickname': name, 'room_name': self.room_name}
1959 name = gobject.markup_escape_text(name)
1961 # We know our contacts nick, but if another contact has the same nick
1962 # in another account we need to also display the account.
1963 # except if we are talking to two different resources of the same contact
1964 acct_info = ''
1965 for account in gajim.contacts.get_accounts():
1966 if account == self.account:
1967 continue
1968 if acct_info: # We already found a contact with same nick
1969 break
1970 for jid in gajim.contacts.get_jid_list(account):
1971 other_contact_ = \
1972 gajim.contacts.get_first_contact_from_jid(account, jid)
1973 if other_contact_.get_shown_name() == self.contact.get_shown_name():
1974 acct_info = ' (%s)' % \
1975 gobject.markup_escape_text(self.account)
1976 break
1978 status = contact.status
1979 if status is not None:
1980 banner_name_label.set_ellipsize(pango.ELLIPSIZE_END)
1981 self.banner_status_label.set_ellipsize(pango.ELLIPSIZE_END)
1982 status_reduced = helpers.reduce_chars_newlines(status, max_lines = 1)
1983 else:
1984 status_reduced = ''
1985 status_escaped = gobject.markup_escape_text(status_reduced)
1987 font_attrs, font_attrs_small = self.get_font_attrs()
1988 st = gajim.config.get('displayed_chat_state_notifications')
1989 cs = contact.chatstate
1990 if cs and st in ('composing_only', 'all'):
1991 if contact.show == 'offline':
1992 chatstate = ''
1993 elif contact.composing_xep == 'XEP-0085':
1994 if st == 'all' or cs == 'composing':
1995 chatstate = helpers.get_uf_chatstate(cs)
1996 else:
1997 chatstate = ''
1998 elif contact.composing_xep == 'XEP-0022':
1999 if cs in ('composing', 'paused'):
2000 # only print composing, paused
2001 chatstate = helpers.get_uf_chatstate(cs)
2002 else:
2003 chatstate = ''
2004 else:
2005 # When does that happen ? See [7797] and [7804]
2006 chatstate = helpers.get_uf_chatstate(cs)
2008 label_text = '<span %s>%s</span><span %s>%s %s</span>' \
2009 % (font_attrs, name, font_attrs_small,
2010 acct_info, chatstate)
2011 if acct_info:
2012 acct_info = ' ' + acct_info
2013 label_tooltip = '%s%s %s' % (name, acct_info, chatstate)
2014 else:
2015 # weight="heavy" size="x-large"
2016 label_text = '<span %s>%s</span><span %s>%s</span>' % \
2017 (font_attrs, name, font_attrs_small, acct_info)
2018 if acct_info:
2019 acct_info = ' ' + acct_info
2020 label_tooltip = '%s%s' % (name, acct_info)
2022 if status_escaped:
2023 status_text = self.urlfinder.sub(self.make_href, status_escaped)
2024 status_text = '<span %s>%s</span>' % (font_attrs_small, status_escaped)
2025 self.banner_status_label.set_tooltip_text(status)
2026 self.banner_status_label.set_no_show_all(False)
2027 self.banner_status_label.show()
2028 else:
2029 status_text = ''
2030 self.banner_status_label.hide()
2031 self.banner_status_label.set_no_show_all(True)
2033 self.banner_status_label.set_markup(status_text)
2034 # setup the label that holds name and jid
2035 banner_name_label.set_markup(label_text)
2036 banner_name_label.set_tooltip_text(label_tooltip)
2038 def close_jingle_content(self, jingle_type):
2039 sid = getattr(self, jingle_type + '_sid')
2040 if not sid:
2041 return
2042 setattr(self, jingle_type + '_sid', None)
2043 setattr(self, jingle_type + '_state', self.JINGLE_STATE_NULL)
2044 session = gajim.connections[self.account].get_jingle_session(
2045 self.contact.get_full_jid(), sid)
2046 if session:
2047 content = session.get_content(jingle_type)
2048 if content:
2049 session.remove_content(content.creator, content.name)
2050 getattr(self, '_' + jingle_type + '_button').set_active(False)
2051 getattr(self, 'update_' + jingle_type)()
2053 def on_jingle_button_toggled(self, widget, jingle_type):
2054 img_name = 'gajim-%s_%s' % ({'audio': 'mic', 'video': 'cam'}[jingle_type],
2055 {True: 'active', False: 'inactive'}[widget.get_active()])
2056 path_to_img = gtkgui_helpers.get_icon_path(img_name)
2058 if widget.get_active():
2059 if getattr(self, jingle_type + '_state') == \
2060 self.JINGLE_STATE_NULL:
2061 sid = getattr(gajim.connections[self.account],
2062 'start_' + jingle_type)(self.contact.get_full_jid())
2063 getattr(self, 'set_' + jingle_type + '_state')('connecting', sid)
2064 else:
2065 self.close_jingle_content(jingle_type)
2067 img = getattr(self, '_' + jingle_type + '_button').get_property('image')
2068 img.set_from_file(path_to_img)
2070 def on_audio_button_toggled(self, widget):
2071 self.on_jingle_button_toggled(widget, 'audio')
2073 def on_video_button_toggled(self, widget):
2074 self.on_jingle_button_toggled(widget, 'video')
2076 def _toggle_gpg(self):
2077 if not self.gpg_is_active and not self.contact.keyID:
2078 dialogs.ErrorDialog(_('No GPG key assigned'),
2079 _('No GPG key is assigned to this contact. So you cannot '
2080 'encrypt messages with GPG.'))
2081 return
2082 ec = gajim.encrypted_chats[self.account]
2083 if self.gpg_is_active:
2084 # Disable encryption
2085 ec.remove(self.contact.jid)
2086 self.gpg_is_active = False
2087 loggable = False
2088 msg = _('GPG encryption disabled')
2089 ChatControlBase.print_conversation_line(self, msg,
2090 'status', '', None)
2091 if self.session:
2092 self.session.loggable = True
2094 else:
2095 # Enable encryption
2096 ec.append(self.contact.jid)
2097 self.gpg_is_active = True
2098 msg = _('GPG encryption enabled')
2099 ChatControlBase.print_conversation_line(self, msg,
2100 'status', '', None)
2102 loggable = gajim.config.get_per('accounts', self.account,
2103 'log_encrypted_sessions')
2105 if self.session:
2106 self.session.loggable = loggable
2108 loggable = self.session.is_loggable()
2109 else:
2110 loggable = loggable and gajim.config.should_log(self.account,
2111 self.contact.jid)
2113 if loggable:
2114 msg = _('Session WILL be logged')
2115 else:
2116 msg = _('Session WILL NOT be logged')
2118 ChatControlBase.print_conversation_line(self, msg,
2119 'status', '', None)
2121 gajim.config.set_per('contacts', self.contact.jid,
2122 'gpg_enabled', self.gpg_is_active)
2124 self._show_lock_image(self.gpg_is_active, 'GPG',
2125 self.gpg_is_active, loggable, True)
2127 def _show_lock_image(self, visible, enc_type = '', enc_enabled = False,
2128 chat_logged = False, authenticated = False):
2130 Set lock icon visibility and create tooltip
2132 #encryption %s active
2133 status_string = enc_enabled and _('is') or _('is NOT')
2134 #chat session %s be logged
2135 logged_string = chat_logged and _('will') or _('will NOT')
2137 if authenticated:
2138 #About encrypted chat session
2139 authenticated_string = _('and authenticated')
2140 img_path = gtkgui_helpers.get_icon_path('gajim-security_high')
2141 else:
2142 #About encrypted chat session
2143 authenticated_string = _('and NOT authenticated')
2144 img_path = gtkgui_helpers.get_icon_path('gajim-security_low')
2145 self.lock_image.set_from_file(img_path)
2147 #status will become 'is' or 'is not', authentificaed will become
2148 #'and authentificated' or 'and not authentificated', logged will become
2149 #'will' or 'will not'
2150 tooltip = _('%(type)s encryption %(status)s active %(authenticated)s.\n'
2151 'Your chat session %(logged)s be logged.') % {'type': enc_type,
2152 'status': status_string, 'authenticated': authenticated_string,
2153 'logged': logged_string}
2155 self.authentication_button.set_tooltip_text(tooltip)
2156 self.widget_set_visible(self.authentication_button, not visible)
2157 self.lock_image.set_sensitive(enc_enabled)
2159 def _on_authentication_button_clicked(self, widget):
2160 if self.gpg_is_active:
2161 dialogs.GPGInfoWindow(self)
2162 elif self.session and self.session.enable_encryption:
2163 dialogs.ESessionInfoWindow(self.session)
2165 def send_message(self, message, keyID='', chatstate=None, xhtml=None,
2166 process_commands=True):
2168 Send a message to contact
2170 if message in ('', None, '\n'):
2171 return None
2173 # refresh timers
2174 self.reset_kbd_mouse_timeout_vars()
2176 contact = self.contact
2178 encrypted = bool(self.session) and self.session.enable_encryption
2180 keyID = ''
2181 if self.gpg_is_active:
2182 keyID = contact.keyID
2183 encrypted = True
2184 if not keyID:
2185 keyID = 'UNKNOWN'
2187 chatstates_on = gajim.config.get('outgoing_chat_state_notifications') != \
2188 'disabled'
2189 composing_xep = contact.composing_xep
2190 chatstate_to_send = None
2191 if chatstates_on and contact is not None:
2192 if composing_xep is None:
2193 # no info about peer
2194 # send active to discover chat state capabilities
2195 # this is here (and not in send_chatstate)
2196 # because we want it sent with REAL message
2197 # (not standlone) eg. one that has body
2199 if contact.our_chatstate:
2200 # We already asked for xep 85, don't ask it twice
2201 composing_xep = 'asked_once'
2203 chatstate_to_send = 'active'
2204 contact.our_chatstate = 'ask' # pseudo state
2205 # if peer supports jep85 and we are not 'ask', send 'active'
2206 # NOTE: first active and 'ask' is set in gajim.py
2207 elif composing_xep is not False:
2208 # send active chatstate on every message (as XEP says)
2209 chatstate_to_send = 'active'
2210 contact.our_chatstate = 'active'
2212 gobject.source_remove(self.possible_paused_timeout_id)
2213 gobject.source_remove(self.possible_inactive_timeout_id)
2214 self._schedule_activity_timers()
2216 def _on_sent(id_, contact, message, encrypted, xhtml, label):
2217 if contact.supports(NS_RECEIPTS) and gajim.config.get_per('accounts',
2218 self.account, 'request_receipt'):
2219 xep0184_id = id_
2220 else:
2221 xep0184_id = None
2222 if label:
2223 displaymarking = label.getTag('displaymarking')
2224 else:
2225 displaymarking = None
2226 self.print_conversation(message, self.contact.jid, encrypted=encrypted,
2227 xep0184_id=xep0184_id, xhtml=xhtml, displaymarking=displaymarking)
2229 ChatControlBase.send_message(self, message, keyID, type_='chat',
2230 chatstate=chatstate_to_send, composing_xep=composing_xep,
2231 xhtml=xhtml, callback=_on_sent,
2232 callback_args=[contact, message, encrypted, xhtml, self.get_seclabel()],
2233 process_commands=process_commands)
2235 def check_for_possible_paused_chatstate(self, arg):
2237 Did we move mouse of that window or write something in message textview
2238 in the last 5 seconds? If yes - we go active for mouse, composing for
2239 kbd. If not - we go paused if we were previously composing
2241 contact = self.contact
2242 jid = contact.jid
2243 current_state = contact.our_chatstate
2244 if current_state is False: # jid doesn't support chatstates
2245 return False # stop looping
2247 message_buffer = self.msg_textview.get_buffer()
2248 if self.kbd_activity_in_last_5_secs and message_buffer.get_char_count():
2249 # Only composing if the keyboard activity was in text entry
2250 self.send_chatstate('composing')
2251 elif self.mouse_over_in_last_5_secs and current_state == 'inactive' and\
2252 jid == self.parent_win.get_active_jid():
2253 self.send_chatstate('active')
2254 else:
2255 if current_state == 'composing':
2256 self.send_chatstate('paused') # pause composing
2258 # assume no activity and let the motion-notify or 'insert-text' make them
2259 # True refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds!
2260 self.reset_kbd_mouse_timeout_vars()
2261 return True # loop forever
2263 def check_for_possible_inactive_chatstate(self, arg):
2265 Did we move mouse over that window or wrote something in message textview
2266 in the last 30 seconds? if yes - we go active. If no - we go inactive
2268 contact = self.contact
2270 current_state = contact.our_chatstate
2271 if current_state is False: # jid doesn't support chatstates
2272 return False # stop looping
2274 if self.mouse_over_in_last_5_secs or self.kbd_activity_in_last_5_secs:
2275 return True # loop forever
2277 if not self.mouse_over_in_last_30_secs or \
2278 self.kbd_activity_in_last_30_secs:
2279 self.send_chatstate('inactive', contact)
2281 # assume no activity and let the motion-notify or 'insert-text' make them
2282 # True refresh 30 seconds too or else it's 30 - 5 = 25 seconds!
2283 self.reset_kbd_mouse_timeout_vars()
2284 return True # loop forever
2286 def reset_kbd_mouse_timeout_vars(self):
2287 self.kbd_activity_in_last_5_secs = False
2288 self.mouse_over_in_last_5_secs = False
2289 self.mouse_over_in_last_30_secs = False
2290 self.kbd_activity_in_last_30_secs = False
2292 def on_cancel_session_negotiation(self):
2293 msg = _('Session negotiation cancelled')
2294 ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
2296 def print_archiving_session_details(self):
2298 Print esession settings to textview
2300 archiving = bool(self.session) and isinstance(self.session,
2301 ArchivingStanzaSession) and self.session.archiving
2302 if archiving:
2303 msg = _('This session WILL be archived on server')
2304 else:
2305 msg = _('This session WILL NOT be archived on server')
2306 ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
2308 def print_esession_details(self):
2310 Print esession settings to textview
2312 e2e_is_active = bool(self.session) and self.session.enable_encryption
2313 if e2e_is_active:
2314 msg = _('This session is encrypted')
2316 if self.session.is_loggable():
2317 msg += _(' and WILL be logged')
2318 else:
2319 msg += _(' and WILL NOT be logged')
2321 ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
2323 if not self.session.verified_identity:
2324 ChatControlBase.print_conversation_line(self, _("Remote contact's identity not verified. Click the shield button for more details."), 'status', '', None)
2325 else:
2326 msg = _('E2E encryption disabled')
2327 ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
2329 self._show_lock_image(e2e_is_active, 'E2E', e2e_is_active, self.session and \
2330 self.session.is_loggable(), self.session and self.session.verified_identity)
2332 def print_session_details(self):
2333 if isinstance(self.session, EncryptedStanzaSession):
2334 self.print_esession_details()
2335 elif isinstance(self.session, ArchivingStanzaSession):
2336 self.print_archiving_session_details()
2338 def print_conversation(self, text, frm='', tim=None, encrypted=False,
2339 subject=None, xhtml=None, simple=False, xep0184_id=None,
2340 displaymarking=None):
2342 Print a line in the conversation
2344 If frm is set to status: it's a status message.
2345 if frm is set to error: it's an error message. The difference between
2346 status and error is mainly that with error, msg count as a new message
2347 (in systray and in control).
2348 If frm is set to info: it's a information message.
2349 If frm is set to print_queue: it is incomming from queue.
2350 If frm is set to another value: it's an outgoing message.
2351 If frm is not set: it's an incomming message.
2353 contact = self.contact
2355 if frm == 'status':
2356 if not gajim.config.get('print_status_in_chats'):
2357 return
2358 kind = 'status'
2359 name = ''
2360 elif frm == 'error':
2361 kind = 'error'
2362 name = ''
2363 elif frm == 'info':
2364 kind = 'info'
2365 name = ''
2366 else:
2367 if self.session and self.session.enable_encryption:
2368 # ESessions
2369 if not encrypted:
2370 msg = _('The following message was NOT encrypted')
2371 ChatControlBase.print_conversation_line(self, msg, 'status', '',
2372 tim)
2373 else:
2374 # GPG encryption
2375 if encrypted and not self.gpg_is_active:
2376 msg = _('The following message was encrypted')
2377 ChatControlBase.print_conversation_line(self, msg, 'status', '',
2378 tim)
2379 # turn on OpenPGP if this was in fact a XEP-0027 encrypted message
2380 if encrypted == 'xep27':
2381 self._toggle_gpg()
2382 elif not encrypted and self.gpg_is_active:
2383 msg = _('The following message was NOT encrypted')
2384 ChatControlBase.print_conversation_line(self, msg, 'status', '',
2385 tim)
2386 if not frm:
2387 kind = 'incoming'
2388 name = contact.get_shown_name()
2389 elif frm == 'print_queue': # incoming message, but do not update time
2390 kind = 'incoming_queue'
2391 name = contact.get_shown_name()
2392 else:
2393 kind = 'outgoing'
2394 name = gajim.nicks[self.account]
2395 if not xhtml and not (encrypted and self.gpg_is_active) and \
2396 gajim.config.get('rst_formatting_outgoing_messages'):
2397 from common.rst_xhtml_generator import create_xhtml
2398 xhtml = create_xhtml(text)
2399 if xhtml:
2400 xhtml = '<body xmlns="%s">%s</body>' % (NS_XHTML, xhtml)
2401 ChatControlBase.print_conversation_line(self, text, kind, name, tim,
2402 subject=subject, old_kind=self.old_msg_kind, xhtml=xhtml,
2403 simple=simple, xep0184_id=xep0184_id, displaymarking=displaymarking)
2404 if text.startswith('/me ') or text.startswith('/me\n'):
2405 self.old_msg_kind = None
2406 else:
2407 self.old_msg_kind = kind
2409 def get_tab_label(self, chatstate):
2410 unread = ''
2411 if self.resource:
2412 jid = self.contact.get_full_jid()
2413 else:
2414 jid = self.contact.jid
2415 num_unread = len(gajim.events.get_events(self.account, jid,
2416 ['printed_' + self.type_id, self.type_id]))
2417 if num_unread == 1 and not gajim.config.get('show_unread_tab_icon'):
2418 unread = '*'
2419 elif num_unread > 1:
2420 unread = '[' + unicode(num_unread) + ']'
2422 # Draw tab label using chatstate
2423 theme = gajim.config.get('roster_theme')
2424 color = None
2425 if not chatstate:
2426 chatstate = self.contact.chatstate
2427 if chatstate is not None:
2428 if chatstate == 'composing':
2429 color = gajim.config.get_per('themes', theme,
2430 'state_composing_color')
2431 elif chatstate == 'inactive':
2432 color = gajim.config.get_per('themes', theme,
2433 'state_inactive_color')
2434 elif chatstate == 'gone':
2435 color = gajim.config.get_per('themes', theme,
2436 'state_gone_color')
2437 elif chatstate == 'paused':
2438 color = gajim.config.get_per('themes', theme,
2439 'state_paused_color')
2440 if color:
2441 # We set the color for when it's the current tab or not
2442 color = gtk.gdk.colormap_get_system().alloc_color(color)
2443 # In inactive tab color to be lighter against the darker inactive
2444 # background
2445 if chatstate in ('inactive', 'gone') and\
2446 self.parent_win.get_active_control() != self:
2447 color = self.lighten_color(color)
2448 else: # active or not chatstate, get color from gtk
2449 color = self.parent_win.notebook.style.fg[gtk.STATE_ACTIVE]
2452 name = self.contact.get_shown_name()
2453 if self.resource:
2454 name += '/' + self.resource
2455 label_str = gobject.markup_escape_text(name)
2456 if num_unread: # if unread, text in the label becomes bold
2457 label_str = '<b>' + unread + label_str + '</b>'
2458 return (label_str, color)
2460 def get_tab_image(self, count_unread=True):
2461 if self.resource:
2462 jid = self.contact.get_full_jid()
2463 else:
2464 jid = self.contact.jid
2465 if count_unread:
2466 num_unread = len(gajim.events.get_events(self.account, jid,
2467 ['printed_' + self.type_id, self.type_id]))
2468 else:
2469 num_unread = 0
2470 # Set tab image (always 16x16); unread messages show the 'event' image
2471 tab_img = None
2473 if num_unread and gajim.config.get('show_unread_tab_icon'):
2474 img_16 = gajim.interface.roster.get_appropriate_state_images(
2475 self.contact.jid, icon_name = 'event')
2476 tab_img = img_16['event']
2477 else:
2478 contact = gajim.contacts.get_contact_with_highest_priority(
2479 self.account, self.contact.jid)
2480 if not contact or self.resource:
2481 # For transient contacts
2482 contact = self.contact
2483 img_16 = gajim.interface.roster.get_appropriate_state_images(
2484 self.contact.jid, icon_name=contact.show)
2485 tab_img = img_16[contact.show]
2487 return tab_img
2489 def prepare_context_menu(self, hide_buttonbar_items=False):
2491 Set compact view menuitem active state sets active and sensitivity state
2492 for toggle_gpg_menuitem sets sensitivity for history_menuitem (False for
2493 tranasports) and file_transfer_menuitem and hide()/show() for
2494 add_to_roster_menuitem
2496 menu = gui_menu_builder.get_contact_menu(self.contact, self.account,
2497 use_multiple_contacts=False, show_start_chat=False,
2498 show_encryption=True, control=self,
2499 show_buttonbar_items=not hide_buttonbar_items)
2500 return menu
2502 def send_chatstate(self, state, contact = None):
2504 Send OUR chatstate as STANDLONE chat state message (eg. no body)
2505 to contact only if new chatstate is different from the previous one
2506 if jid is not specified, send to active tab
2508 # JEP 85 does not allow resending the same chatstate
2509 # this function checks for that and just returns so it's safe to call it
2510 # with same state.
2512 # This functions also checks for violation in state transitions
2513 # and raises RuntimeException with appropriate message
2514 # more on that http://www.jabber.org/jeps/jep-0085.html#statechart
2516 # do not send nothing if we have chat state notifications disabled
2517 # that means we won't reply to the <active/> from other peer
2518 # so we do not broadcast jep85 capabalities
2519 chatstate_setting = gajim.config.get('outgoing_chat_state_notifications')
2520 if chatstate_setting == 'disabled':
2521 return
2522 elif chatstate_setting == 'composing_only' and state != 'active' and\
2523 state != 'composing':
2524 return
2526 if contact is None:
2527 contact = self.parent_win.get_active_contact()
2528 if contact is None:
2529 # contact was from pm in MUC, and left the room so contact is None
2530 # so we cannot send chatstate anymore
2531 return
2533 # Don't send chatstates to offline contacts
2534 if contact.show == 'offline':
2535 return
2537 if contact.composing_xep is False: # jid cannot do xep85 nor xep22
2538 return
2540 # if the new state we wanna send (state) equals
2541 # the current state (contact.our_chatstate) then return
2542 if contact.our_chatstate == state:
2543 return
2545 if contact.composing_xep is None:
2546 # we don't know anything about jid, so return
2547 # NOTE:
2548 # send 'active', set current state to 'ask' and return is done
2549 # in self.send_message() because we need REAL message (with <body>)
2550 # for that procedure so return to make sure we send only once
2551 # 'active' until we know peer supports jep85
2552 return
2554 if contact.our_chatstate == 'ask':
2555 return
2557 # in JEP22, when we already sent stop composing
2558 # notification on paused, don't resend it
2559 if contact.composing_xep == 'XEP-0022' and \
2560 contact.our_chatstate in ('paused', 'active', 'inactive') and \
2561 state is not 'composing': # not composing == in (active, inactive, gone)
2562 contact.our_chatstate = 'active'
2563 self.reset_kbd_mouse_timeout_vars()
2564 return
2566 # if we're inactive prevent composing (JEP violation)
2567 if contact.our_chatstate == 'inactive' and state == 'composing':
2568 # go active before
2569 MessageControl.send_message(self, None, chatstate = 'active')
2570 contact.our_chatstate = 'active'
2571 self.reset_kbd_mouse_timeout_vars()
2573 MessageControl.send_message(self, None, chatstate = state,
2574 msg_id = contact.msg_id, composing_xep = contact.composing_xep)
2575 contact.our_chatstate = state
2576 if contact.our_chatstate == 'active':
2577 self.reset_kbd_mouse_timeout_vars()
2579 def shutdown(self):
2580 # PluginSystem: calling shutdown of super class (ChatControlBase) to let it remove
2581 # it's GUI extension points
2582 super(ChatControl, self).shutdown()
2583 # PluginSystem: removing GUI extension points connected with ChatControl
2584 # instance object
2585 gajim.plugin_manager.remove_gui_extension_point('chat_control', self) # Send 'gone' chatstate
2587 gajim.ged.remove_event_handler('pep-received', ged.GUI1,
2588 self._nec_pep_received)
2589 gajim.ged.remove_event_handler('vcard-received', ged.GUI1,
2590 self._nec_vcard_received)
2591 gajim.ged.remove_event_handler('failed-decrypt', ged.GUI1,
2592 self._nec_failed_decrypt)
2593 gajim.ged.remove_event_handler('chatstate-received', ged.GUI1,
2594 self._nec_chatstate_received)
2595 gajim.ged.remove_event_handler('caps-received', ged.GUI1,
2596 self._nec_caps_received)
2598 self.send_chatstate('gone', self.contact)
2599 self.contact.chatstate = None
2600 self.contact.our_chatstate = None
2602 for jingle_type in ('audio', 'video'):
2603 self.close_jingle_content(jingle_type)
2605 # disconnect self from session
2606 if self.session:
2607 self.session.control = None
2609 # Disconnect timer callbacks
2610 gobject.source_remove(self.possible_paused_timeout_id)
2611 gobject.source_remove(self.possible_inactive_timeout_id)
2612 # Remove bigger avatar window
2613 if self.bigger_avatar_window:
2614 self.bigger_avatar_window.destroy()
2615 # Clean events
2616 gajim.events.remove_events(self.account, self.get_full_jid(),
2617 types = ['printed_' + self.type_id, self.type_id])
2618 # Remove contact instance if contact has been removed
2619 key = (self.contact.jid, self.account)
2620 roster = gajim.interface.roster
2621 if key in roster.contacts_to_be_removed.keys() and \
2622 not roster.contact_has_pending_roster_events(self.contact, self.account):
2623 backend = roster.contacts_to_be_removed[key]['backend']
2624 del roster.contacts_to_be_removed[key]
2625 roster.remove_contact(self.contact.jid, self.account, force=True,
2626 backend=backend)
2627 # remove all register handlers on widgets, created by self.xml
2628 # to prevent circular references among objects
2629 for i in self.handlers.keys():
2630 if self.handlers[i].handler_is_connected(i):
2631 self.handlers[i].disconnect(i)
2632 del self.handlers[i]
2633 self.conv_textview.del_handlers()
2634 if gajim.config.get('use_speller') and HAS_GTK_SPELL:
2635 spell_obj = gtkspell.get_from_text_view(self.msg_textview)
2636 if spell_obj:
2637 spell_obj.detach()
2638 self.msg_textview.destroy()
2640 def minimizable(self):
2641 return False
2643 def safe_shutdown(self):
2644 return False
2646 def allow_shutdown(self, method, on_yes, on_no, on_minimize):
2647 if time.time() - gajim.last_message_time[self.account]\
2648 [self.get_full_jid()] < 2:
2649 # 2 seconds
2650 def on_ok():
2651 on_yes(self)
2653 def on_cancel():
2654 on_no(self)
2656 dialogs.ConfirmationDialog(
2657 # %s is being replaced in the code with JID
2658 _('You just received a new message from "%s"') % self.contact.jid,
2659 _('If you close this tab and you have history disabled, '\
2660 'this message will be lost.'), on_response_ok=on_ok,
2661 on_response_cancel=on_cancel)
2662 return
2663 on_yes(self)
2665 def _nec_chatstate_received(self, obj):
2667 Handle incoming chatstate that jid SENT TO us
2669 self.draw_banner_text()
2670 # update chatstate in tab for this chat
2671 self.parent_win.redraw_tab(self, self.contact.chatstate)
2673 def _nec_caps_received(self, obj):
2674 if obj.conn.name != self.account or obj.jid != self.contact.jid:
2675 return
2676 self.update_ui()
2678 def set_control_active(self, state):
2679 ChatControlBase.set_control_active(self, state)
2680 # send chatstate inactive to the one we're leaving
2681 # and active to the one we visit
2682 if state:
2683 message_buffer = self.msg_textview.get_buffer()
2684 if message_buffer.get_char_count():
2685 self.send_chatstate('paused', self.contact)
2686 else:
2687 self.send_chatstate('active', self.contact)
2688 self.reset_kbd_mouse_timeout_vars()
2689 gobject.source_remove(self.possible_paused_timeout_id)
2690 gobject.source_remove(self.possible_inactive_timeout_id)
2691 self._schedule_activity_timers()
2692 else:
2693 self.send_chatstate('inactive', self.contact)
2694 # Hide bigger avatar window
2695 if self.bigger_avatar_window:
2696 self.bigger_avatar_window.destroy()
2697 self.bigger_avatar_window = None
2698 # Re-show the small avatar
2699 self.show_avatar()
2701 def show_avatar(self):
2702 if not gajim.config.get('show_avatar_in_chat'):
2703 return
2705 jid_with_resource = self.contact.get_full_jid()
2706 pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid_with_resource)
2707 if pixbuf == 'ask':
2708 # we don't have the vcard
2709 if self.TYPE_ID == message_control.TYPE_PM:
2710 if self.gc_contact.jid:
2711 # We know the real jid of this contact
2712 real_jid = self.gc_contact.jid
2713 if self.gc_contact.resource:
2714 real_jid += '/' + self.gc_contact.resource
2715 else:
2716 real_jid = jid_with_resource
2717 gajim.connections[self.account].request_vcard(real_jid,
2718 jid_with_resource)
2719 else:
2720 gajim.connections[self.account].request_vcard(jid_with_resource)
2721 return
2722 elif pixbuf:
2723 scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'chat')
2724 else:
2725 scaled_pixbuf = None
2727 image = self.xml.get_object('avatar_image')
2728 image.set_from_pixbuf(scaled_pixbuf)
2729 image.show_all()
2731 def _nec_vcard_received(self, obj):
2732 if obj.conn.name != self.account:
2733 return
2734 j = gajim.get_jid_without_resource(self.contact.jid)
2735 if obj.jid != j:
2736 return
2737 self.show_avatar()
2739 def _on_drag_data_received(self, widget, context, x, y, selection,
2740 target_type, timestamp):
2741 if not selection.data:
2742 return
2743 if self.TYPE_ID == message_control.TYPE_PM:
2744 c = self.gc_contact
2745 else:
2746 c = self.contact
2747 if target_type == self.TARGET_TYPE_URI_LIST:
2748 if not c.resource: # If no resource is known, we can't send a file
2749 return
2750 uri = selection.data.strip()
2751 uri_splitted = uri.split() # we may have more than one file dropped
2752 for uri in uri_splitted:
2753 path = helpers.get_file_path_from_dnd_dropped_uri(uri)
2754 if os.path.isfile(path): # is it file?
2755 ft = gajim.interface.instances['file_transfers']
2756 ft.send_file(self.account, c, path)
2757 return
2759 # chat2muc
2760 treeview = gajim.interface.roster.tree
2761 model = treeview.get_model()
2762 data = selection.data
2763 path = treeview.get_selection().get_selected_rows()[1][0]
2764 iter_ = model.get_iter(path)
2765 type_ = model[iter_][2]
2766 if type_ != 'contact': # source is not a contact
2767 return
2768 dropped_jid = data.decode('utf-8')
2770 dropped_transport = gajim.get_transport_name_from_jid(dropped_jid)
2771 c_transport = gajim.get_transport_name_from_jid(c.jid)
2772 if dropped_transport or c_transport:
2773 return # transport contacts cannot be invited
2775 dialogs.TransformChatToMUC(self.account, [c.jid], [dropped_jid])
2777 def _on_message_tv_buffer_changed(self, textbuffer):
2778 self.kbd_activity_in_last_5_secs = True
2779 self.kbd_activity_in_last_30_secs = True
2780 if textbuffer.get_char_count():
2781 self.send_chatstate('composing', self.contact)
2783 e2e_is_active = self.session and \
2784 self.session.enable_encryption
2785 e2e_pref = gajim.config.get_per('accounts', self.account,
2786 'enable_esessions') and gajim.config.get_per('accounts',
2787 self.account, 'autonegotiate_esessions') and gajim.config.get_per(
2788 'contacts', self.contact.jid, 'autonegotiate_esessions')
2789 want_e2e = not e2e_is_active and not self.gpg_is_active \
2790 and e2e_pref
2792 if want_e2e and not self.no_autonegotiation \
2793 and gajim.HAVE_PYCRYPTO and self.contact.supports(NS_ESESSION):
2794 self.begin_e2e_negotiation()
2795 elif (not self.session or not self.session.status) and \
2796 gajim.connections[self.account].archiving_supported:
2797 self.begin_archiving_negotiation()
2798 else:
2799 self.send_chatstate('active', self.contact)
2801 def restore_conversation(self):
2802 jid = self.contact.jid
2803 # don't restore lines if it's a transport
2804 if gajim.jid_is_transport(jid):
2805 return
2807 # How many lines to restore and when to time them out
2808 restore_how_many = gajim.config.get('restore_lines')
2809 if restore_how_many <= 0:
2810 return
2811 timeout = gajim.config.get('restore_timeout') # in minutes
2813 # number of messages that are in queue and are already logged, we want
2814 # to avoid duplication
2815 pending_how_many = len(gajim.events.get_events(self.account, jid,
2816 ['chat', 'pm']))
2817 if self.resource:
2818 pending_how_many += len(gajim.events.get_events(self.account,
2819 self.contact.get_full_jid(), ['chat', 'pm']))
2821 try:
2822 rows = gajim.logger.get_last_conversation_lines(jid, restore_how_many,
2823 pending_how_many, timeout, self.account)
2824 except exceptions.DatabaseMalformed:
2825 import common.logger
2826 dialogs.ErrorDialog(_('Database Error'),
2827 _('The database file (%s) cannot be read. Try to repair it or remove it (all history will be lost).') % common.logger.LOG_DB_PATH)
2828 rows = []
2829 local_old_kind = None
2830 for row in rows: # row[0] time, row[1] has kind, row[2] the message
2831 if not row[2]: # message is empty, we don't print it
2832 continue
2833 if row[1] in (constants.KIND_CHAT_MSG_SENT,
2834 constants.KIND_SINGLE_MSG_SENT):
2835 kind = 'outgoing'
2836 name = gajim.nicks[self.account]
2837 elif row[1] in (constants.KIND_SINGLE_MSG_RECV,
2838 constants.KIND_CHAT_MSG_RECV):
2839 kind = 'incoming'
2840 name = self.contact.get_shown_name()
2841 elif row[1] == constants.KIND_ERROR:
2842 kind = 'status'
2843 name = self.contact.get_shown_name()
2845 tim = time.localtime(float(row[0]))
2847 if gajim.config.get('restored_messages_small'):
2848 small_attr = ['small']
2849 else:
2850 small_attr = []
2851 ChatControlBase.print_conversation_line(self, row[2], kind, name, tim,
2852 small_attr,
2853 small_attr + ['restored_message'],
2854 small_attr + ['restored_message'],
2855 False, old_kind = local_old_kind)
2856 if row[2].startswith('/me ') or row[2].startswith('/me\n'):
2857 local_old_kind = None
2858 else:
2859 local_old_kind = kind
2860 if len(rows):
2861 self.conv_textview.print_empty_line()
2863 def read_queue(self):
2865 Read queue and print messages containted in it
2867 jid = self.contact.jid
2868 jid_with_resource = jid
2869 if self.resource:
2870 jid_with_resource += '/' + self.resource
2871 events = gajim.events.get_events(self.account, jid_with_resource)
2873 # list of message ids which should be marked as read
2874 message_ids = []
2875 for event in events:
2876 if event.type_ != self.type_id:
2877 continue
2878 data = event.parameters
2879 kind = data[2]
2880 if kind == 'error':
2881 kind = 'info'
2882 else:
2883 kind = 'print_queue'
2884 dm = None
2885 if len(data) > 10:
2886 dm = data[10]
2887 self.print_conversation(data[0], kind, tim = data[3],
2888 encrypted = data[4], subject = data[1], xhtml = data[7],
2889 displaymarking=dm)
2890 if len(data) > 6 and isinstance(data[6], int):
2891 message_ids.append(data[6])
2893 if len(data) > 8:
2894 self.set_session(data[8])
2895 if message_ids:
2896 gajim.logger.set_read_messages(message_ids)
2897 gajim.events.remove_events(self.account, jid_with_resource,
2898 types = [self.type_id])
2900 typ = 'chat' # Is it a normal chat or a pm ?
2902 # reset to status image in gc if it is a pm
2903 # Is it a pm ?
2904 room_jid, nick = gajim.get_room_and_nick_from_fjid(jid)
2905 control = gajim.interface.msg_win_mgr.get_gc_control(room_jid,
2906 self.account)
2907 if control and control.type_id == message_control.TYPE_GC:
2908 control.update_ui()
2909 control.parent_win.show_title()
2910 typ = 'pm'
2912 self.redraw_after_event_removed(jid)
2913 if (self.contact.show in ('offline', 'error')):
2914 show_offline = gajim.config.get('showoffline')
2915 show_transports = gajim.config.get('show_transports_group')
2916 if (not show_transports and gajim.jid_is_transport(jid)) or \
2917 (not show_offline and typ == 'chat' and \
2918 len(gajim.contacts.get_contacts(self.account, jid)) < 2):
2919 gajim.interface.roster.remove_to_be_removed(self.contact.jid,
2920 self.account)
2921 elif typ == 'pm':
2922 control.remove_contact(nick)
2924 def show_bigger_avatar(self, small_avatar):
2926 Resize the avatar, if needed, so it has at max half the screen size and
2927 shows it
2929 if not small_avatar.window:
2930 # Tab has been closed since we hovered the avatar
2931 return
2932 avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(
2933 self.contact.jid)
2934 if avatar_pixbuf in ('ask', None):
2935 return
2936 # Hide the small avatar
2937 # this code hides the small avatar when we show a bigger one in case
2938 # the avatar has a transparency hole in the middle
2939 # so when we show the big one we avoid seeing the small one behind.
2940 # It's why I set it transparent.
2941 image = self.xml.get_object('avatar_image')
2942 pixbuf = image.get_pixbuf()
2943 pixbuf.fill(0xffffff00L) # RGBA
2944 image.queue_draw()
2946 screen_w = gtk.gdk.screen_width()
2947 screen_h = gtk.gdk.screen_height()
2948 avatar_w = avatar_pixbuf.get_width()
2949 avatar_h = avatar_pixbuf.get_height()
2950 half_scr_w = screen_w / 2
2951 half_scr_h = screen_h / 2
2952 if avatar_w > half_scr_w:
2953 avatar_w = half_scr_w
2954 if avatar_h > half_scr_h:
2955 avatar_h = half_scr_h
2956 window = gtk.Window(gtk.WINDOW_POPUP)
2957 self.bigger_avatar_window = window
2958 pixmap, mask = avatar_pixbuf.render_pixmap_and_mask()
2959 window.set_size_request(avatar_w, avatar_h)
2960 # we should make the cursor visible
2961 # gtk+ doesn't make use of the motion notify on gtkwindow by default
2962 # so this line adds that
2963 window.set_events(gtk.gdk.POINTER_MOTION_MASK)
2964 window.set_app_paintable(True)
2965 window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_TOOLTIP)
2967 window.realize()
2968 window.window.set_back_pixmap(pixmap, False) # make it transparent
2969 window.window.shape_combine_mask(mask, 0, 0)
2971 # make the bigger avatar window show up centered
2972 x0, y0 = small_avatar.window.get_origin()
2973 x0 += small_avatar.allocation.x
2974 y0 += small_avatar.allocation.y
2975 center_x= x0 + (small_avatar.allocation.width / 2)
2976 center_y = y0 + (small_avatar.allocation.height / 2)
2977 pos_x, pos_y = center_x - (avatar_w / 2), center_y - (avatar_h / 2)
2978 window.move(pos_x, pos_y)
2979 # make the cursor invisible so we can see the image
2980 invisible_cursor = gtkgui_helpers.get_invisible_cursor()
2981 window.window.set_cursor(invisible_cursor)
2983 # we should hide the window
2984 window.connect('leave_notify_event',
2985 self._on_window_avatar_leave_notify_event)
2986 window.connect('motion-notify-event',
2987 self._on_window_motion_notify_event)
2989 window.show_all()
2991 def _on_window_avatar_leave_notify_event(self, widget, event):
2993 Just left the popup window that holds avatar
2995 self.bigger_avatar_window.destroy()
2996 self.bigger_avatar_window = None
2997 # Re-show the small avatar
2998 self.show_avatar()
3000 def _on_window_motion_notify_event(self, widget, event):
3002 Just moved the mouse so show the cursor
3004 cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)
3005 self.bigger_avatar_window.window.set_cursor(cursor)
3007 def _on_send_file_menuitem_activate(self, widget):
3008 self._on_send_file()
3010 def _on_add_to_roster_menuitem_activate(self, widget):
3011 dialogs.AddNewContactWindow(self.account, self.contact.jid)
3013 def _on_contact_information_menuitem_activate(self, widget):
3014 gajim.interface.roster.on_info(widget, self.contact, self.account)
3016 def _on_toggle_gpg_menuitem_activate(self, widget):
3017 self._toggle_gpg()
3019 def _on_convert_to_gc_menuitem_activate(self, widget):
3021 User wants to invite some friends to chat
3023 dialogs.TransformChatToMUC(self.account, [self.contact.jid])
3025 def _on_toggle_e2e_menuitem_activate(self, widget):
3026 if self.session and self.session.enable_encryption:
3027 # e2e was enabled, disable it
3028 jid = str(self.session.jid)
3029 thread_id = self.session.thread_id
3031 self.session.terminate_e2e()
3033 gajim.connections[self.account].delete_session(jid, thread_id)
3035 # presumably the user had a good reason to shut it off, so
3036 # disable autonegotiation too
3037 self.no_autonegotiation = True
3038 else:
3039 self.begin_e2e_negotiation()
3041 def begin_negotiation(self):
3042 self.no_autonegotiation = True
3044 if not self.session:
3045 fjid = self.contact.get_full_jid()
3046 new_sess = gajim.connections[self.account].make_new_session(fjid, type_=self.type_id)
3047 self.set_session(new_sess)
3049 def begin_e2e_negotiation(self):
3050 self.begin_negotiation()
3051 self.session.negotiate_e2e(False)
3053 def begin_archiving_negotiation(self):
3054 self.begin_negotiation()
3055 self.session.negotiate_archiving()
3057 def _nec_failed_decrypt(self, obj):
3058 if obj.session != self.session:
3059 return
3061 details = _('Unable to decrypt message from %s\nIt may have been '
3062 'tampered with.') % obj.fjid
3063 self.print_conversation_line(details, 'status', '', obj.timestamp)
3065 # terminate the session
3066 thread_id = self.session.thread_id
3067 self.session.terminate_e2e()
3068 obj.conn.delete_session(obj.fjid, thread_id)
3070 # restart the session
3071 self.begin_e2e_negotiation()
3073 # Stop emission so it doesn't go to gui_interface
3074 return True
3076 def got_connected(self):
3077 ChatControlBase.got_connected(self)
3078 # Refreshing contact
3079 contact = gajim.contacts.get_contact_with_highest_priority(
3080 self.account, self.contact.jid)
3081 if isinstance(contact, GC_Contact):
3082 contact = contact.as_contact()
3083 if contact:
3084 self.contact = contact
3085 self.draw_banner()
3087 def update_status_display(self, name, uf_show, status):
3089 Print the contact's status and update the status/GPG image
3091 self.update_ui()
3092 self.parent_win.redraw_tab(self)
3094 self.print_conversation(_('%(name)s is now %(status)s') % {'name': name,
3095 'status': uf_show}, 'status')
3097 if status:
3098 self.print_conversation(' (', 'status', simple=True)
3099 self.print_conversation('%s' % (status), 'status', simple=True)
3100 self.print_conversation(')', 'status', simple=True)