use NEC to handle our-status event
[gajim.git] / src / chat_control.py
blobfe586cf1ea47f40c07bfe2e66aa6211c079e0e46
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 handle_message_textview_mykey_press(self, widget, event_keyval,
216 event_keymod):
218 Derives types SHOULD implement this, rather than connection to the even
219 itself
221 event = gtk.gdk.Event(gtk.gdk.KEY_PRESS)
222 event.keyval = event_keyval
223 event.state = event_keymod
224 event.time = 0
226 _buffer = widget.get_buffer()
227 start, end = _buffer.get_bounds()
229 if event.keyval -- gtk.keysyms.Tab:
230 position = _buffer.get_insert()
231 end = _buffer.get_iter_at_mark(position)
233 text = _buffer.get_text(start, end, False)
234 text = text.decode('utf8')
236 splitted = text.split()
238 if (text.startswith(self.COMMAND_PREFIX) and not
239 text.startswith(self.COMMAND_PREFIX * 2) and len(splitted) == 1):
241 text = splitted[0]
242 bare = text.lstrip(self.COMMAND_PREFIX)
244 if len(text) == 1:
245 self.command_hits = []
246 for command in self.list_commands():
247 for name in command.names:
248 self.command_hits.append(name)
249 else:
250 if (self.last_key_tabs and self.command_hits and
251 self.command_hits[0].startswith(bare)):
252 self.command_hits.append(self.command_hits.pop(0))
253 else:
254 self.command_hits = []
255 for command in self.list_commands():
256 for name in command.names:
257 if name.startswith(bare):
258 self.command_hits.append(name)
260 if self.command_hits:
261 _buffer.delete(start, end)
262 _buffer.insert_at_cursor(self.COMMAND_PREFIX + self.command_hits[0] + ' ')
263 self.last_key_tabs = True
265 return True
267 self.last_key_tabs = False
269 def status_url_clicked(self, widget, url):
270 helpers.launch_browser_mailer('url', url)
272 def setup_seclabel(self, combo):
273 self.seclabel_combo = combo
274 self.seclabel_combo.hide()
275 self.seclabel_combo.set_no_show_all(True)
276 lb = gtk.ListStore(str)
277 self.seclabel_combo.set_model(lb)
278 cell = gtk.CellRendererText()
279 cell.set_property('xpad', 5) # padding for status text
280 self.seclabel_combo.pack_start(cell, True)
281 # text to show is in in first column of liststore
282 self.seclabel_combo.add_attribute(cell, 'text', 0)
283 if gajim.connections[self.account].seclabel_supported:
284 gajim.connections[self.account].seclabel_catalogue(self.contact.jid, self.on_seclabels_ready)
286 def on_seclabels_ready(self):
287 lb = self.seclabel_combo.get_model()
288 lb.clear()
289 for label in gajim.connections[self.account].seclabel_catalogues[self.contact.jid][2]:
290 lb.append([label])
291 self.seclabel_combo.set_active(0)
292 self.seclabel_combo.set_no_show_all(False)
293 self.seclabel_combo.show_all()
295 def __init__(self, type_id, parent_win, widget_name, contact, acct,
296 resource=None):
297 # Undo needs this variable to know if space has been pressed.
298 # Initialize it to True so empty textview is saved in undo list
299 self.space_pressed = True
301 if resource is None:
302 # We very likely got a contact with a random resource.
303 # This is bad, we need the highest for caps etc.
304 c = gajim.contacts.get_contact_with_highest_priority(
305 acct, contact.jid)
306 if c and not isinstance(c, GC_Contact):
307 contact = c
309 MessageControl.__init__(self, type_id, parent_win, widget_name,
310 contact, acct, resource=resource)
312 widget = self.xml.get_object('history_button')
313 id_ = widget.connect('clicked', self._on_history_menuitem_activate)
314 self.handlers[id_] = widget
316 # when/if we do XHTML we will put formatting buttons back
317 widget = self.xml.get_object('emoticons_button')
318 id_ = widget.connect('clicked', self.on_emoticons_button_clicked)
319 self.handlers[id_] = widget
321 # Create banner and connect signals
322 widget = self.xml.get_object('banner_eventbox')
323 id_ = widget.connect('button-press-event',
324 self._on_banner_eventbox_button_press_event)
325 self.handlers[id_] = widget
327 self.urlfinder = re.compile(
328 r"(www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'\"]+[^!,\.\s<>\)'\"\]]")
330 self.banner_status_label = self.xml.get_object('banner_label')
331 id_ = self.banner_status_label.connect('populate_popup',
332 self.on_banner_label_populate_popup)
333 self.handlers[id_] = self.banner_status_label
335 # Init DND
336 self.TARGET_TYPE_URI_LIST = 80
337 self.dnd_list = [ ( 'text/uri-list', 0, self.TARGET_TYPE_URI_LIST ),
338 ('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_APP, 0)]
339 id_ = self.widget.connect('drag_data_received',
340 self._on_drag_data_received)
341 self.handlers[id_] = self.widget
342 self.widget.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
343 gtk.DEST_DEFAULT_HIGHLIGHT |
344 gtk.DEST_DEFAULT_DROP,
345 self.dnd_list, gtk.gdk.ACTION_COPY)
347 # Create textviews and connect signals
348 self.conv_textview = ConversationTextview(self.account)
349 id_ = self.conv_textview.connect('quote', self.on_quote)
350 self.handlers[id_] = self.conv_textview.tv
351 id_ = self.conv_textview.tv.connect('key_press_event',
352 self._conv_textview_key_press_event)
353 self.handlers[id_] = self.conv_textview.tv
354 # FIXME: DND on non editable TextView, find a better way
355 self.drag_entered = False
356 id_ = self.conv_textview.tv.connect('drag_data_received',
357 self._on_drag_data_received)
358 self.handlers[id_] = self.conv_textview.tv
359 id_ = self.conv_textview.tv.connect('drag_motion', self._on_drag_motion)
360 self.handlers[id_] = self.conv_textview.tv
361 id_ = self.conv_textview.tv.connect('drag_leave', self._on_drag_leave)
362 self.handlers[id_] = self.conv_textview.tv
363 self.conv_textview.tv.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
364 gtk.DEST_DEFAULT_HIGHLIGHT |
365 gtk.DEST_DEFAULT_DROP,
366 self.dnd_list, gtk.gdk.ACTION_COPY)
368 self.conv_scrolledwindow = self.xml.get_object(
369 'conversation_scrolledwindow')
370 self.conv_scrolledwindow.add(self.conv_textview.tv)
371 widget = self.conv_scrolledwindow.get_vadjustment()
372 id_ = widget.connect('value-changed',
373 self.on_conversation_vadjustment_value_changed)
374 self.handlers[id_] = widget
375 id_ = widget.connect('changed',
376 self.on_conversation_vadjustment_changed)
377 self.handlers[id_] = widget
378 self.scroll_to_end_id = None
379 self.was_at_the_end = True
381 # add MessageTextView to UI and connect signals
382 self.msg_scrolledwindow = self.xml.get_object('message_scrolledwindow')
383 self.msg_textview = MessageTextView()
384 id_ = self.msg_textview.connect('mykeypress',
385 self._on_message_textview_mykeypress_event)
386 self.handlers[id_] = self.msg_textview
387 self.msg_scrolledwindow.add(self.msg_textview)
388 id_ = self.msg_textview.connect('key_press_event',
389 self._on_message_textview_key_press_event)
390 self.handlers[id_] = self.msg_textview
391 id_ = self.msg_textview.connect('size-request', self.size_request)
392 self.handlers[id_] = self.msg_textview
393 id_ = self.msg_textview.connect('populate_popup',
394 self.on_msg_textview_populate_popup)
395 self.handlers[id_] = self.msg_textview
396 # Setup DND
397 id_ = self.msg_textview.connect('drag_data_received',
398 self._on_drag_data_received)
399 self.handlers[id_] = self.msg_textview
400 self.msg_textview.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
401 gtk.DEST_DEFAULT_HIGHLIGHT,
402 self.dnd_list, gtk.gdk.ACTION_COPY)
404 self.update_font()
406 # Hook up send button
407 widget = self.xml.get_object('send_button')
408 id_ = widget.connect('clicked', self._on_send_button_clicked)
409 self.handlers[id_] = widget
411 widget = self.xml.get_object('formattings_button')
412 id_ = widget.connect('clicked', self.on_formattings_button_clicked)
413 self.handlers[id_] = widget
415 # the following vars are used to keep history of user's messages
416 self.sent_history = []
417 self.sent_history_pos = 0
418 self.orig_msg = None
420 # Emoticons menu
421 # set image no matter if user wants at this time emoticons or not
422 # (so toggle works ok)
423 img = self.xml.get_object('emoticons_button_image')
424 img.set_from_file(os.path.join(gajim.DATA_DIR, 'emoticons', 'static',
425 'smile.png'))
426 self.toggle_emoticons()
428 # Attach speller
429 if gajim.config.get('use_speller') and HAS_GTK_SPELL:
430 self.set_speller()
431 self.conv_textview.tv.show()
432 self._paint_banner()
434 # For XEP-0172
435 self.user_nick = None
437 self.smooth = True
438 self.msg_textview.grab_focus()
440 self.command_hits = []
441 self.last_key_tabs = False
443 # PluginSystem: adding GUI extension point for ChatControlBase
444 # instance object (also subclasses, eg. ChatControl or GroupchatControl)
445 gajim.plugin_manager.gui_extension_point('chat_control_base', self)
447 gajim.ged.register_event_handler('our-show', ged.GUI1,
448 self._nec_our_status)
450 # This is bascially a very nasty hack to surpass the inability
451 # to properly use the super, because of the old code.
452 CommandTools.__init__(self)
454 def set_speller(self):
455 # now set the one the user selected
456 per_type = 'contacts'
457 if self.type_id == message_control.TYPE_GC:
458 per_type = 'rooms'
459 lang = gajim.config.get_per(per_type, self.contact.jid,
460 'speller_language')
461 if not lang:
462 # use the default one
463 lang = gajim.config.get('speller_language')
464 if not lang:
465 lang = gajim.LANG
466 if lang:
467 try:
468 gtkspell.Spell(self.msg_textview, lang)
469 self.msg_textview.lang = lang
470 except (gobject.GError, RuntimeError, TypeError, OSError):
471 dialogs.AspellDictError(lang)
473 def on_banner_label_populate_popup(self, label, menu):
475 Override the default context menu and add our own menutiems
477 item = gtk.SeparatorMenuItem()
478 menu.prepend(item)
480 menu2 = self.prepare_context_menu()
481 i = 0
482 for item in menu2:
483 menu2.remove(item)
484 menu.prepend(item)
485 menu.reorder_child(item, i)
486 i += 1
487 menu.show_all()
489 def shutdown(self):
490 # PluginSystem: removing GUI extension points connected with ChatControlBase
491 # instance object
492 gajim.plugin_manager.remove_gui_extension_point('chat_control_base', self)
493 gajim.plugin_manager.remove_gui_extension_point('chat_control_base_draw_banner', self)
494 gajim.ged.remove_event_handler('our-show', ged.GUI1,
495 self._nec_our_status)
497 def on_msg_textview_populate_popup(self, textview, menu):
499 Override the default context menu and we prepend an option to switch
500 languages
502 def _on_select_dictionary(widget, lang):
503 per_type = 'contacts'
504 if self.type_id == message_control.TYPE_GC:
505 per_type = 'rooms'
506 if not gajim.config.get_per(per_type, self.contact.jid):
507 gajim.config.add_per(per_type, self.contact.jid)
508 gajim.config.set_per(per_type, self.contact.jid, 'speller_language',
509 lang)
510 spell = gtkspell.get_from_text_view(self.msg_textview)
511 self.msg_textview.lang = lang
512 spell.set_language(lang)
513 widget.set_active(True)
515 item = gtk.ImageMenuItem(gtk.STOCK_UNDO)
516 menu.prepend(item)
517 id_ = item.connect('activate', self.msg_textview.undo)
518 self.handlers[id_] = item
520 item = gtk.SeparatorMenuItem()
521 menu.prepend(item)
523 item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
524 menu.prepend(item)
525 id_ = item.connect('activate', self.msg_textview.clear)
526 self.handlers[id_] = item
528 if gajim.config.get('use_speller') and HAS_GTK_SPELL:
529 item = gtk.MenuItem(_('Spelling language'))
530 menu.prepend(item)
531 submenu = gtk.Menu()
532 item.set_submenu(submenu)
533 for lang in sorted(langs):
534 item = gtk.CheckMenuItem(lang)
535 if langs[lang] == self.msg_textview.lang:
536 item.set_active(True)
537 submenu.append(item)
538 id_ = item.connect('activate', _on_select_dictionary, langs[lang])
539 self.handlers[id_] = item
541 menu.show_all()
543 def on_quote(self, widget, text):
544 text = '>' + text.replace('\n', '\n>') + '\n'
545 message_buffer = self.msg_textview.get_buffer()
546 message_buffer.insert_at_cursor(text)
548 # moved from ChatControl
549 def _on_banner_eventbox_button_press_event(self, widget, event):
551 If right-clicked, show popup
553 if event.button == 3: # right click
554 self.parent_win.popup_menu(event)
556 def _on_send_button_clicked(self, widget):
558 When send button is pressed: send the current message
560 if gajim.connections[self.account].connected < 2: # we are not connected
561 dialogs.ErrorDialog(_('A connection is not available'),
562 _('Your message can not be sent until you are connected.'))
563 return
564 message_buffer = self.msg_textview.get_buffer()
565 start_iter = message_buffer.get_start_iter()
566 end_iter = message_buffer.get_end_iter()
567 message = message_buffer.get_text(start_iter, end_iter, 0).decode('utf-8')
568 xhtml = self.msg_textview.get_xhtml()
570 # send the message
571 self.send_message(message, xhtml=xhtml)
573 def _paint_banner(self):
575 Repaint banner with theme color
577 theme = gajim.config.get('roster_theme')
578 bgcolor = gajim.config.get_per('themes', theme, 'bannerbgcolor')
579 textcolor = gajim.config.get_per('themes', theme, 'bannertextcolor')
580 # the backgrounds are colored by using an eventbox by
581 # setting the bg color of the eventbox and the fg of the name_label
582 banner_eventbox = self.xml.get_object('banner_eventbox')
583 banner_name_label = self.xml.get_object('banner_name_label')
584 self.disconnect_style_event(banner_name_label)
585 self.disconnect_style_event(self.banner_status_label)
586 if bgcolor:
587 banner_eventbox.modify_bg(gtk.STATE_NORMAL,
588 gtk.gdk.color_parse(bgcolor))
589 default_bg = False
590 else:
591 default_bg = True
592 if textcolor:
593 banner_name_label.modify_fg(gtk.STATE_NORMAL,
594 gtk.gdk.color_parse(textcolor))
595 self.banner_status_label.modify_fg(gtk.STATE_NORMAL,
596 gtk.gdk.color_parse(textcolor))
597 default_fg = False
598 else:
599 default_fg = True
600 if default_bg or default_fg:
601 self._on_style_set_event(banner_name_label, None, default_fg,
602 default_bg)
603 if self.banner_status_label.flags() & gtk.REALIZED:
604 # Widget is realized
605 self._on_style_set_event(self.banner_status_label, None, default_fg,
606 default_bg)
608 def disconnect_style_event(self, widget):
609 # Try to find the event_id
610 for id_ in self.handlers.keys():
611 if self.handlers[id_] == widget:
612 widget.disconnect(id_)
613 del self.handlers[id_]
614 break
616 def connect_style_event(self, widget, set_fg = False, set_bg = False):
617 self.disconnect_style_event(widget)
618 id_ = widget.connect('style-set', self._on_style_set_event, set_fg,
619 set_bg)
620 self.handlers[id_] = widget
622 def _on_style_set_event(self, widget, style, *opts):
624 Set style of widget from style class *.Frame.Eventbox
625 opts[0] == True -> set fg color
626 opts[1] == True -> set bg color
628 banner_eventbox = self.xml.get_object('banner_eventbox')
629 self.disconnect_style_event(widget)
630 if opts[1]:
631 bg_color = widget.style.bg[gtk.STATE_SELECTED]
632 banner_eventbox.modify_bg(gtk.STATE_NORMAL, bg_color)
633 if opts[0]:
634 fg_color = widget.style.fg[gtk.STATE_SELECTED]
635 widget.modify_fg(gtk.STATE_NORMAL, fg_color)
636 self.connect_style_event(widget, opts[0], opts[1])
638 def _conv_textview_key_press_event(self, widget, event):
639 # translate any layout to latin_layout
640 keymap = gtk.gdk.keymap_get_default()
641 keycode = keymap.get_entries_for_keyval(event.keyval)[0][0]
642 if (event.state & gtk.gdk.CONTROL_MASK and keycode in (self.keycode_c,
643 self.keycode_ins)) or (event.state & gtk.gdk.SHIFT_MASK and \
644 event.keyval in (gtk.keysyms.Page_Down, gtk.keysyms.Page_Up)):
645 return False
646 self.parent_win.notebook.emit('key_press_event', event)
647 return True
649 def show_emoticons_menu(self):
650 if not gajim.config.get('emoticons_theme'):
651 return
652 def set_emoticons_menu_position(w, msg_tv = self.msg_textview):
653 window = msg_tv.get_window(gtk.TEXT_WINDOW_WIDGET)
654 # get the window position
655 origin = window.get_origin()
656 size = window.get_size()
657 buf = msg_tv.get_buffer()
658 # get the cursor position
659 cursor = msg_tv.get_iter_location(buf.get_iter_at_mark(
660 buf.get_insert()))
661 cursor = msg_tv.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
662 cursor.x, cursor.y)
663 x = origin[0] + cursor[0]
664 y = origin[1] + size[1]
665 menu_height = gajim.interface.emoticons_menu.size_request()[1]
666 #FIXME: get_line_count is not so good
667 #get the iter of cursor, then tv.get_line_yrange
668 # so we know in which y we are typing (not how many lines we have
669 # then go show just above the current cursor line for up
670 # or just below the current cursor line for down
671 #TEST with having 3 lines and writing in the 2nd
672 if y + menu_height > gtk.gdk.screen_height():
673 # move menu just above cursor
674 y -= menu_height + (msg_tv.allocation.height / buf.get_line_count())
675 #else: # move menu just below cursor
676 # y -= (msg_tv.allocation.height / buf.get_line_count())
677 return (x, y, True) # push_in True
678 gajim.interface.emoticon_menuitem_clicked = self.append_emoticon
679 gajim.interface.emoticons_menu.popup(None, None,
680 set_emoticons_menu_position, 1, 0)
682 def _on_message_textview_key_press_event(self, widget, event):
683 if event.keyval == gtk.keysyms.space:
684 self.space_pressed = True
686 elif (self.space_pressed or self.msg_textview.undo_pressed) and \
687 event.keyval not in (gtk.keysyms.Control_L, gtk.keysyms.Control_R) and \
688 not (event.keyval == gtk.keysyms.z and event.state & gtk.gdk.CONTROL_MASK):
689 # If the space key has been pressed and now it hasnt,
690 # we save the buffer into the undo list. But be carefull we're not
691 # pressiong Control again (as in ctrl+z)
692 _buffer = widget.get_buffer()
693 start_iter, end_iter = _buffer.get_bounds()
694 self.msg_textview.save_undo(_buffer.get_text(start_iter, end_iter))
695 self.space_pressed = False
697 # Ctrl [+ Shift] + Tab are not forwarded to notebook. We handle it here
698 if self.widget_name == 'groupchat_control':
699 if event.keyval not in (gtk.keysyms.ISO_Left_Tab, gtk.keysyms.Tab):
700 self.last_key_tabs = False
701 if event.state & gtk.gdk.SHIFT_MASK:
702 # CTRL + SHIFT + TAB
703 if event.state & gtk.gdk.CONTROL_MASK and \
704 event.keyval == gtk.keysyms.ISO_Left_Tab:
705 self.parent_win.move_to_next_unread_tab(False)
706 return True
707 # SHIFT + PAGE_[UP|DOWN]: send to conv_textview
708 elif event.keyval == gtk.keysyms.Page_Down or \
709 event.keyval == gtk.keysyms.Page_Up:
710 self.conv_textview.tv.emit('key_press_event', event)
711 return True
712 elif event.state & gtk.gdk.CONTROL_MASK:
713 if event.keyval == gtk.keysyms.Tab: # CTRL + TAB
714 self.parent_win.move_to_next_unread_tab(True)
715 return True
716 return False
718 def _on_message_textview_mykeypress_event(self, widget, event_keyval,
719 event_keymod):
721 When a key is pressed: if enter is pressed without the shift key, message
722 (if not empty) is sent and printed in the conversation
724 # NOTE: handles mykeypress which is custom signal connected to this
725 # CB in new_tab(). for this singal see message_textview.py
726 message_textview = widget
727 message_buffer = message_textview.get_buffer()
728 start_iter, end_iter = message_buffer.get_bounds()
729 message = message_buffer.get_text(start_iter, end_iter, False).decode(
730 'utf-8')
731 xhtml = self.msg_textview.get_xhtml()
733 # construct event instance from binding
734 event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) # it's always a key-press here
735 event.keyval = event_keyval
736 event.state = event_keymod
737 event.time = 0 # assign current time
739 if event.keyval == gtk.keysyms.Up:
740 if event.state & gtk.gdk.CONTROL_MASK: # Ctrl+UP
741 self.sent_messages_scroll('up', widget.get_buffer())
742 elif event.keyval == gtk.keysyms.Down:
743 if event.state & gtk.gdk.CONTROL_MASK: # Ctrl+Down
744 self.sent_messages_scroll('down', widget.get_buffer())
745 elif event.keyval == gtk.keysyms.Return or \
746 event.keyval == gtk.keysyms.KP_Enter: # ENTER
747 # NOTE: SHIFT + ENTER is not needed to be emulated as it is not
748 # binding at all (textview's default action is newline)
750 if gajim.config.get('send_on_ctrl_enter'):
751 # here, we emulate GTK default action on ENTER (add new line)
752 # normally I would add in keypress but it gets way to complex
753 # to get instant result on changing this advanced setting
754 if event.state == 0: # no ctrl, no shift just ENTER add newline
755 end_iter = message_buffer.get_end_iter()
756 message_buffer.insert_at_cursor('\n')
757 send_message = False
758 elif event.state & gtk.gdk.CONTROL_MASK: # CTRL + ENTER
759 send_message = True
760 else: # send on Enter, do newline on Ctrl Enter
761 if event.state & gtk.gdk.CONTROL_MASK: # Ctrl + ENTER
762 end_iter = message_buffer.get_end_iter()
763 message_buffer.insert_at_cursor('\n')
764 send_message = False
765 else: # ENTER
766 send_message = True
768 if gajim.connections[self.account].connected < 2 and send_message:
769 # we are not connected
770 dialogs.ErrorDialog(_('A connection is not available'),
771 _('Your message can not be sent until you are connected.'))
772 send_message = False
774 if send_message:
775 self.send_message(message, xhtml=xhtml) # send the message
776 elif event.keyval == gtk.keysyms.z: # CTRL+z
777 if event.state & gtk.gdk.CONTROL_MASK:
778 self.msg_textview.undo()
779 else:
780 # Give the control itself a chance to process
781 self.handle_message_textview_mykey_press(widget, event_keyval,
782 event_keymod)
784 def _on_drag_data_received(self, widget, context, x, y, selection,
785 target_type, timestamp):
787 Derived types SHOULD implement this
789 pass
791 def _on_drag_leave(self, widget, context, time):
792 # FIXME: DND on non editable TextView, find a better way
793 self.drag_entered = False
794 self.conv_textview.tv.set_editable(False)
796 def _on_drag_motion(self, widget, context, x, y, time):
797 # FIXME: DND on non editable TextView, find a better way
798 if not self.drag_entered:
799 # We drag new data over the TextView, make it editable to catch dnd
800 self.drag_entered_conv = True
801 self.conv_textview.tv.set_editable(True)
803 def get_seclabel(self):
804 label = None
805 if self.seclabel_combo is not None:
806 idx = self.seclabel_combo.get_active()
807 if idx != -1:
808 cat = gajim.connections[self.account].seclabel_catalogues[self.contact.jid]
809 lname = cat[2][idx]
810 label = cat[1][lname]
811 return label
813 def send_message(self, message, keyID='', type_='chat', chatstate=None,
814 msg_id=None, composing_xep=None, resource=None, xhtml=None,
815 callback=None, callback_args=[], process_commands=True):
817 Send the given message to the active tab. Doesn't return None if error
819 if not message or message == '\n':
820 return None
822 if process_commands and self.process_as_command(message):
823 return
825 label = self.get_seclabel()
826 MessageControl.send_message(self, message, keyID, type_=type_,
827 chatstate=chatstate, msg_id=msg_id, composing_xep=composing_xep,
828 resource=resource, user_nick=self.user_nick, xhtml=xhtml,
829 label=label,
830 callback=callback, callback_args=callback_args)
832 # Record message history
833 self.save_sent_message(message)
835 # Be sure to send user nickname only once according to JEP-0172
836 self.user_nick = None
838 # Clear msg input
839 message_buffer = self.msg_textview.get_buffer()
840 message_buffer.set_text('') # clear message buffer (and tv of course)
842 def save_sent_message(self, message):
843 # save the message, so user can scroll though the list with key up/down
844 size = len(self.sent_history)
845 # we don't want size of the buffer to grow indefinately
846 max_size = gajim.config.get('key_up_lines')
847 if size >= max_size:
848 for i in xrange(0, size - 1):
849 self.sent_history[i] = self.sent_history[i + 1]
850 self.sent_history[max_size - 1] = message
851 # self.sent_history_pos has changed if we browsed sent_history,
852 # reset to real value
853 self.sent_history_pos = max_size
854 else:
855 self.sent_history.append(message)
856 self.sent_history_pos = size + 1
857 self.orig_msg = None
859 def print_conversation_line(self, text, kind, name, tim,
860 other_tags_for_name=[], other_tags_for_time=[],
861 other_tags_for_text=[], count_as_new=True, subject=None,
862 old_kind=None, xhtml=None, simple=False, xep0184_id=None,
863 graphics=True, displaymarking=None):
865 Print 'chat' type messages
867 jid = self.contact.jid
868 full_jid = self.get_full_jid()
869 textview = self.conv_textview
870 end = False
871 if self.was_at_the_end or kind == 'outgoing':
872 end = True
873 textview.print_conversation_line(text, jid, kind, name, tim,
874 other_tags_for_name, other_tags_for_time, other_tags_for_text,
875 subject, old_kind, xhtml, simple=simple, graphics=graphics,
876 displaymarking=displaymarking)
878 if xep0184_id is not None:
879 textview.show_xep0184_warning(xep0184_id)
881 if not count_as_new:
882 return
883 if kind == 'incoming':
884 if not self.type_id == message_control.TYPE_GC or \
885 gajim.config.get('notify_on_all_muc_messages') or \
886 'marked' in other_tags_for_text:
887 # it's a normal message, or a muc message with want to be
888 # notified about if quitting just after
889 # other_tags_for_text == ['marked'] --> highlighted gc message
890 gajim.last_message_time[self.account][full_jid] = time.time()
892 if kind in ('incoming', 'incoming_queue', 'error'):
893 gc_message = False
894 if self.type_id == message_control.TYPE_GC:
895 gc_message = True
897 if ((self.parent_win and (not self.parent_win.get_active_control() or \
898 self != self.parent_win.get_active_control() or \
899 not self.parent_win.is_active() or not end)) or \
900 (gc_message and \
901 jid in gajim.interface.minimized_controls[self.account])) and \
902 kind in ('incoming', 'incoming_queue', 'error'):
903 # we want to have save this message in events list
904 # other_tags_for_text == ['marked'] --> highlighted gc message
905 if gc_message:
906 if 'marked' in other_tags_for_text:
907 type_ = 'printed_marked_gc_msg'
908 else:
909 type_ = 'printed_gc_msg'
910 event = 'gc_message_received'
911 else:
912 type_ = 'printed_' + self.type_id
913 event = 'message_received'
914 show_in_roster = notify.get_show_in_roster(event,
915 self.account, self.contact, self.session)
916 show_in_systray = notify.get_show_in_systray(event,
917 self.account, self.contact, type_)
919 event = gajim.events.create_event(type_, (self,),
920 show_in_roster = show_in_roster,
921 show_in_systray = show_in_systray)
922 gajim.events.add_event(self.account, full_jid, event)
923 # We need to redraw contact if we show in roster
924 if show_in_roster:
925 gajim.interface.roster.draw_contact(self.contact.jid,
926 self.account)
928 if not self.parent_win:
929 return
931 if (not self.parent_win.get_active_control() or \
932 self != self.parent_win.get_active_control() or \
933 not self.parent_win.is_active() or not end) and \
934 kind in ('incoming', 'incoming_queue', 'error'):
935 self.parent_win.redraw_tab(self)
936 if not self.parent_win.is_active():
937 self.parent_win.show_title(True, self) # Enabled Urgent hint
938 else:
939 self.parent_win.show_title(False, self) # Disabled Urgent hint
941 def toggle_emoticons(self):
943 Hide show emoticons_button and make sure emoticons_menu is always there
944 when needed
946 emoticons_button = self.xml.get_object('emoticons_button')
947 if gajim.config.get('emoticons_theme'):
948 emoticons_button.show()
949 emoticons_button.set_no_show_all(False)
950 else:
951 emoticons_button.hide()
952 emoticons_button.set_no_show_all(True)
954 def append_emoticon(self, str_):
955 buffer_ = self.msg_textview.get_buffer()
956 if buffer_.get_char_count():
957 buffer_.insert_at_cursor(' %s ' % str_)
958 else: # we are the beginning of buffer
959 buffer_.insert_at_cursor('%s ' % str_)
960 self.msg_textview.grab_focus()
962 def on_emoticons_button_clicked(self, widget):
964 Popup emoticons menu
966 gajim.interface.emoticon_menuitem_clicked = self.append_emoticon
967 gajim.interface.popup_emoticons_under_button(widget, self.parent_win)
969 def on_formattings_button_clicked(self, widget):
971 Popup formattings menu
973 menu = gtk.Menu()
975 menuitems = ((_('Bold'), 'bold'),
976 (_('Italic'), 'italic'),
977 (_('Underline'), 'underline'),
978 (_('Strike'), 'strike'))
980 active_tags = self.msg_textview.get_active_tags()
982 for menuitem in menuitems:
983 item = gtk.CheckMenuItem(menuitem[0])
984 if menuitem[1] in active_tags:
985 item.set_active(True)
986 else:
987 item.set_active(False)
988 item.connect('activate', self.msg_textview.set_tag,
989 menuitem[1])
990 menu.append(item)
992 item = gtk.SeparatorMenuItem() # separator
993 menu.append(item)
995 item = gtk.ImageMenuItem(_('Color'))
996 icon = gtk.image_new_from_stock(gtk.STOCK_SELECT_COLOR, gtk.ICON_SIZE_MENU)
997 item.set_image(icon)
998 item.connect('activate', self.on_color_menuitem_activale)
999 menu.append(item)
1001 item = gtk.ImageMenuItem(_('Font'))
1002 icon = gtk.image_new_from_stock(gtk.STOCK_SELECT_FONT, gtk.ICON_SIZE_MENU)
1003 item.set_image(icon)
1004 item.connect('activate', self.on_font_menuitem_activale)
1005 menu.append(item)
1007 item = gtk.SeparatorMenuItem() # separator
1008 menu.append(item)
1010 item = gtk.ImageMenuItem(_('Clear formating'))
1011 icon = gtk.image_new_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU)
1012 item.set_image(icon)
1013 item.connect('activate', self.msg_textview.clear_tags)
1014 menu.append(item)
1016 menu.show_all()
1017 gtkgui_helpers.popup_emoticons_under_button(menu, widget,
1018 self.parent_win)
1020 def on_color_menuitem_activale(self, widget):
1021 color_dialog = gtk.ColorSelectionDialog('Select a color')
1022 color_dialog.connect('response', self.msg_textview.color_set,
1023 color_dialog.colorsel)
1024 color_dialog.show_all()
1026 def on_font_menuitem_activale(self, widget):
1027 font_dialog = gtk.FontSelectionDialog('Select a font')
1028 font_dialog.connect('response', self.msg_textview.font_set,
1029 font_dialog.fontsel)
1030 font_dialog.show_all()
1033 def on_actions_button_clicked(self, widget):
1035 Popup action menu
1037 menu = self.prepare_context_menu(hide_buttonbar_items=True)
1038 menu.show_all()
1039 gtkgui_helpers.popup_emoticons_under_button(menu, widget,
1040 self.parent_win)
1042 def update_font(self):
1043 font = pango.FontDescription(gajim.config.get('conversation_font'))
1044 self.conv_textview.tv.modify_font(font)
1045 self.msg_textview.modify_font(font)
1047 def update_tags(self):
1048 self.conv_textview.update_tags()
1050 def clear(self, tv):
1051 buffer_ = tv.get_buffer()
1052 start, end = buffer_.get_bounds()
1053 buffer_.delete(start, end)
1055 def _on_history_menuitem_activate(self, widget = None, jid = None):
1057 When history menuitem is pressed: call history window
1059 if not jid:
1060 jid = self.contact.jid
1062 if 'logs' in gajim.interface.instances:
1063 gajim.interface.instances['logs'].window.present()
1064 gajim.interface.instances['logs'].open_history(jid, self.account)
1065 else:
1066 gajim.interface.instances['logs'] = \
1067 history_window.HistoryWindow(jid, self.account)
1069 def _on_send_file(self, gc_contact=None):
1071 gc_contact can be set when we are in a groupchat control
1073 def _on_ok(c):
1074 gajim.interface.instances['file_transfers'].show_file_send_request(
1075 self.account, c)
1076 if self.TYPE_ID == message_control.TYPE_PM:
1077 gc_contact = self.gc_contact
1078 if gc_contact:
1079 # gc or pm
1080 gc_control = gajim.interface.msg_win_mgr.get_gc_control(
1081 gc_contact.room_jid, self.account)
1082 self_contact = gajim.contacts.get_gc_contact(self.account,
1083 gc_control.room_jid, gc_control.nick)
1084 if gc_control.is_anonymous and gc_contact.affiliation not in ['admin',
1085 'owner'] and self_contact.affiliation in ['admin', 'owner']:
1086 contact = gajim.contacts.get_contact(self.account, gc_contact.jid)
1087 if not contact or contact.sub not in ('both', 'to'):
1088 prim_text = _('Really send file?')
1089 sec_text = _('If you send a file to %s, he/she will know your '
1090 'real Jabber ID.') % gc_contact.name
1091 dialog = dialogs.NonModalConfirmationDialog(prim_text, sec_text,
1092 on_response_ok = (_on_ok, gc_contact))
1093 dialog.popup()
1094 return
1095 _on_ok(gc_contact)
1096 return
1097 _on_ok(self.contact)
1099 def on_minimize_menuitem_toggled(self, widget):
1101 When a grouchat is minimized, unparent the tab, put it in roster etc
1103 old_value = False
1104 minimized_gc = gajim.config.get_per('accounts', self.account,
1105 'minimized_gc').split()
1106 if self.contact.jid in minimized_gc:
1107 old_value = True
1108 minimize = widget.get_active()
1109 if minimize and not self.contact.jid in minimized_gc:
1110 minimized_gc.append(self.contact.jid)
1111 if not minimize and self.contact.jid in minimized_gc:
1112 minimized_gc.remove(self.contact.jid)
1113 if old_value != minimize:
1114 gajim.config.set_per('accounts', self.account, 'minimized_gc',
1115 ' '.join(minimized_gc))
1117 def set_control_active(self, state):
1118 if state:
1119 jid = self.contact.jid
1120 if self.was_at_the_end:
1121 # we are at the end
1122 type_ = ['printed_' + self.type_id]
1123 if self.type_id == message_control.TYPE_GC:
1124 type_ = ['printed_gc_msg', 'printed_marked_gc_msg']
1125 if not gajim.events.remove_events(self.account, self.get_full_jid(),
1126 types = type_):
1127 # There were events to remove
1128 self.redraw_after_event_removed(jid)
1131 def bring_scroll_to_end(self, textview, diff_y = 0):
1133 Scroll to the end of textview if end is not visible
1135 if self.scroll_to_end_id:
1136 # a scroll is already planned
1137 return
1138 buffer_ = textview.get_buffer()
1139 end_iter = buffer_.get_end_iter()
1140 end_rect = textview.get_iter_location(end_iter)
1141 visible_rect = textview.get_visible_rect()
1142 # scroll only if expected end is not visible
1143 if end_rect.y >= (visible_rect.y + visible_rect.height + diff_y):
1144 self.scroll_to_end_id = gobject.idle_add(self.scroll_to_end_iter,
1145 textview)
1147 def scroll_to_end_iter(self, textview):
1148 buffer_ = textview.get_buffer()
1149 end_iter = buffer_.get_end_iter()
1150 textview.scroll_to_iter(end_iter, 0, False, 1, 1)
1151 self.scroll_to_end_id = None
1152 return False
1154 def size_request(self, msg_textview, requisition):
1156 When message_textview changes its size: if the new height will enlarge
1157 the window, enable the scrollbar automatic policy. Also enable scrollbar
1158 automatic policy for horizontal scrollbar if message we have in
1159 message_textview is too big
1161 if msg_textview.window is None:
1162 return
1164 min_height = self.conv_scrolledwindow.get_property('height-request')
1165 conversation_height = self.conv_textview.tv.window.get_size()[1]
1166 message_height = msg_textview.window.get_size()[1]
1167 message_width = msg_textview.window.get_size()[0]
1168 # new tab is not exposed yet
1169 if conversation_height < 2:
1170 return
1172 if conversation_height < min_height:
1173 min_height = conversation_height
1175 # we don't want to always resize in height the message_textview
1176 # so we have minimum on conversation_textview's scrolled window
1177 # but we also want to avoid window resizing so if we reach that
1178 # minimum for conversation_textview and maximum for message_textview
1179 # we set to automatic the scrollbar policy
1180 diff_y = message_height - requisition.height
1181 if diff_y != 0:
1182 if conversation_height + diff_y < min_height:
1183 if message_height + conversation_height - min_height > min_height:
1184 policy = self.msg_scrolledwindow.get_property(
1185 'vscrollbar-policy')
1186 # scroll only when scrollbar appear
1187 if policy != gtk.POLICY_AUTOMATIC:
1188 self.msg_scrolledwindow.set_property('vscrollbar-policy',
1189 gtk.POLICY_AUTOMATIC)
1190 self.msg_scrolledwindow.set_property('height-request',
1191 message_height + conversation_height - min_height)
1192 self.bring_scroll_to_end(msg_textview)
1193 else:
1194 self.msg_scrolledwindow.set_property('vscrollbar-policy',
1195 gtk.POLICY_NEVER)
1196 self.msg_scrolledwindow.set_property('height-request', -1)
1197 self.conv_textview.bring_scroll_to_end(diff_y - 18, False)
1198 else:
1199 self.conv_textview.bring_scroll_to_end(diff_y - 18, self.smooth)
1200 self.smooth = True # reinit the flag
1201 # enable scrollbar automatic policy for horizontal scrollbar
1202 # if message we have in message_textview is too big
1203 if requisition.width > message_width:
1204 self.msg_scrolledwindow.set_property('hscrollbar-policy',
1205 gtk.POLICY_AUTOMATIC)
1206 else:
1207 self.msg_scrolledwindow.set_property('hscrollbar-policy',
1208 gtk.POLICY_NEVER)
1210 return True
1212 def on_conversation_vadjustment_changed(self, adjustment):
1213 # used to stay at the end of the textview when we shrink conversation
1214 # textview.
1215 if self.was_at_the_end:
1216 self.conv_textview.bring_scroll_to_end(-18)
1217 self.was_at_the_end = (adjustment.upper - adjustment.value - adjustment.page_size) < 18
1219 def on_conversation_vadjustment_value_changed(self, adjustment):
1220 # stop automatic scroll when we manually scroll
1221 if not self.conv_textview.auto_scrolling:
1222 self.conv_textview.stop_scrolling()
1223 self.was_at_the_end = (adjustment.upper - adjustment.value - adjustment.page_size) < 18
1224 if self.resource:
1225 jid = self.contact.get_full_jid()
1226 else:
1227 jid = self.contact.jid
1228 types_list = []
1229 type_ = self.type_id
1230 if type_ == message_control.TYPE_GC:
1231 type_ = 'gc_msg'
1232 types_list = ['printed_' + type_, type_, 'printed_marked_gc_msg']
1233 else: # Not a GC
1234 types_list = ['printed_' + type_, type_]
1236 if not len(gajim.events.get_events(self.account, jid, types_list)):
1237 return
1238 if not self.parent_win:
1239 return
1240 if self.conv_textview.at_the_end() and \
1241 self.parent_win.get_active_control() == self and \
1242 self.parent_win.window.is_active():
1243 # we are at the end
1244 if self.type_id == message_control.TYPE_GC:
1245 if not gajim.events.remove_events(self.account, jid,
1246 types=types_list):
1247 self.redraw_after_event_removed(jid)
1248 elif self.session and self.session.remove_events(types_list):
1249 # There were events to remove
1250 self.redraw_after_event_removed(jid)
1252 def redraw_after_event_removed(self, jid):
1254 We just removed a 'printed_*' event, redraw contact in roster or
1255 gc_roster and titles in roster and msg_win
1257 self.parent_win.redraw_tab(self)
1258 self.parent_win.show_title()
1259 # TODO : get the contact and check notify.get_show_in_roster()
1260 if self.type_id == message_control.TYPE_PM:
1261 room_jid, nick = gajim.get_room_and_nick_from_fjid(jid)
1262 groupchat_control = gajim.interface.msg_win_mgr.get_gc_control(
1263 room_jid, self.account)
1264 if room_jid in gajim.interface.minimized_controls[self.account]:
1265 groupchat_control = \
1266 gajim.interface.minimized_controls[self.account][room_jid]
1267 contact = \
1268 gajim.contacts.get_contact_with_highest_priority(self.account, \
1269 room_jid)
1270 if contact:
1271 gajim.interface.roster.draw_contact(room_jid, self.account)
1272 if groupchat_control:
1273 groupchat_control.draw_contact(nick)
1274 if groupchat_control.parent_win:
1275 groupchat_control.parent_win.redraw_tab(groupchat_control)
1276 else:
1277 gajim.interface.roster.draw_contact(jid, self.account)
1278 gajim.interface.roster.show_title()
1280 def sent_messages_scroll(self, direction, conv_buf):
1281 size = len(self.sent_history)
1282 if self.orig_msg is None:
1283 # user was typing something and then went into history, so save
1284 # whatever is already typed
1285 start_iter = conv_buf.get_start_iter()
1286 end_iter = conv_buf.get_end_iter()
1287 self.orig_msg = conv_buf.get_text(start_iter, end_iter, 0).decode(
1288 'utf-8')
1289 if direction == 'up':
1290 if self.sent_history_pos == 0:
1291 return
1292 self.sent_history_pos = self.sent_history_pos - 1
1293 self.smooth = False
1294 conv_buf.set_text(self.sent_history[self.sent_history_pos])
1295 elif direction == 'down':
1296 if self.sent_history_pos >= size - 1:
1297 conv_buf.set_text(self.orig_msg)
1298 self.orig_msg = None
1299 self.sent_history_pos = size
1300 return
1302 self.sent_history_pos = self.sent_history_pos + 1
1303 self.smooth = False
1304 conv_buf.set_text(self.sent_history[self.sent_history_pos])
1306 def lighten_color(self, color):
1307 p = 0.4
1308 mask = 0
1309 color.red = int((color.red * p) + (mask * (1 - p)))
1310 color.green = int((color.green * p) + (mask * (1 - p)))
1311 color.blue = int((color.blue * p) + (mask * (1 - p)))
1312 return color
1314 def widget_set_visible(self, widget, state):
1316 Show or hide a widget
1318 # make the last message visible, when changing to "full view"
1319 if not state:
1320 gobject.idle_add(self.conv_textview.scroll_to_end_iter)
1322 widget.set_no_show_all(state)
1323 if state:
1324 widget.hide()
1325 else:
1326 widget.show_all()
1328 def chat_buttons_set_visible(self, state):
1330 Toggle chat buttons
1332 MessageControl.chat_buttons_set_visible(self, state)
1333 self.widget_set_visible(self.xml.get_object('actions_hbox'), state)
1335 def got_connected(self):
1336 self.msg_textview.set_sensitive(True)
1337 self.msg_textview.set_editable(True)
1338 # FIXME: Set sensitivity for toolbar
1340 def got_disconnected(self):
1341 self.msg_textview.set_sensitive(False)
1342 self.msg_textview.set_editable(False)
1343 self.conv_textview.tv.grab_focus()
1345 self.no_autonegotiation = False
1346 # FIXME: Set sensitivity for toolbar
1348 ################################################################################
1349 class ChatControl(ChatControlBase):
1351 A control for standard 1-1 chat
1354 JINGLE_STATE_NULL,
1355 JINGLE_STATE_CONNECTING,
1356 JINGLE_STATE_CONNECTION_RECEIVED,
1357 JINGLE_STATE_CONNECTED,
1358 JINGLE_STATE_ERROR
1359 ) = range(5)
1361 TYPE_ID = message_control.TYPE_CHAT
1362 old_msg_kind = None # last kind of the printed message
1364 # Set a command host to bound to. Every command given through a chat will be
1365 # processed with this command host.
1366 COMMAND_HOST = ChatCommands
1368 def __init__(self, parent_win, contact, acct, session, resource = None):
1369 ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
1370 'chat_control', contact, acct, resource)
1372 self.gpg_is_active = False
1373 # for muc use:
1374 # widget = self.xml.get_object('muc_window_actions_button')
1375 self.actions_button = self.xml.get_object('message_window_actions_button')
1376 id_ = self.actions_button.connect('clicked',
1377 self.on_actions_button_clicked)
1378 self.handlers[id_] = self.actions_button
1380 self._formattings_button = self.xml.get_object('formattings_button')
1382 self._add_to_roster_button = self.xml.get_object(
1383 'add_to_roster_button')
1384 id_ = self._add_to_roster_button.connect('clicked',
1385 self._on_add_to_roster_menuitem_activate)
1386 self.handlers[id_] = self._add_to_roster_button
1388 self._audio_button = self.xml.get_object('audio_togglebutton')
1389 id_ = self._audio_button.connect('toggled', self.on_audio_button_toggled)
1390 self.handlers[id_] = self._audio_button
1391 # add a special img
1392 gtkgui_helpers.add_image_to_button(self._audio_button,
1393 'gajim-mic_inactive')
1395 self._video_button = self.xml.get_object('video_togglebutton')
1396 id_ = self._video_button.connect('toggled', self.on_video_button_toggled)
1397 self.handlers[id_] = self._video_button
1398 # add a special img
1399 gtkgui_helpers.add_image_to_button(self._video_button,
1400 'gajim-cam_inactive')
1402 self._send_file_button = self.xml.get_object('send_file_button')
1403 # add a special img for send file button
1404 path_to_upload_img = gtkgui_helpers.get_icon_path('gajim-upload')
1405 img = gtk.Image()
1406 img.set_from_file(path_to_upload_img)
1407 self._send_file_button.set_image(img)
1408 id_ = self._send_file_button.connect('clicked',
1409 self._on_send_file_menuitem_activate)
1410 self.handlers[id_] = self._send_file_button
1412 self._convert_to_gc_button = self.xml.get_object(
1413 'convert_to_gc_button')
1414 id_ = self._convert_to_gc_button.connect('clicked',
1415 self._on_convert_to_gc_menuitem_activate)
1416 self.handlers[id_] = self._convert_to_gc_button
1418 contact_information_button = self.xml.get_object(
1419 'contact_information_button')
1420 id_ = contact_information_button.connect('clicked',
1421 self._on_contact_information_menuitem_activate)
1422 self.handlers[id_] = contact_information_button
1424 compact_view = gajim.config.get('compact_view')
1425 self.chat_buttons_set_visible(compact_view)
1426 self.widget_set_visible(self.xml.get_object('banner_eventbox'),
1427 gajim.config.get('hide_chat_banner'))
1429 self.authentication_button = self.xml.get_object(
1430 'authentication_button')
1431 id_ = self.authentication_button.connect('clicked',
1432 self._on_authentication_button_clicked)
1433 self.handlers[id_] = self.authentication_button
1435 # Add lock image to show chat encryption
1436 self.lock_image = self.xml.get_object('lock_image')
1438 # Convert to GC icon
1439 img = self.xml.get_object('convert_to_gc_button_image')
1440 img.set_from_pixbuf(gtkgui_helpers.load_icon(
1441 'muc_active').get_pixbuf())
1443 self._audio_banner_image = self.xml.get_object('audio_banner_image')
1444 self._video_banner_image = self.xml.get_object('video_banner_image')
1445 self.audio_sid = None
1446 self.audio_state = self.JINGLE_STATE_NULL
1447 self.audio_available = False
1448 self.video_sid = None
1449 self.video_state = self.JINGLE_STATE_NULL
1450 self.video_available = False
1452 self.update_toolbar()
1454 self._pep_images = {}
1455 self._pep_images['mood'] = self.xml.get_object('mood_image')
1456 self._pep_images['activity'] = self.xml.get_object('activity_image')
1457 self._pep_images['tune'] = self.xml.get_object('tune_image')
1458 self._pep_images['location'] = self.xml.get_object('location_image')
1459 self.update_all_pep_types()
1461 # keep timeout id and window obj for possible big avatar
1462 # it is on enter-notify and leave-notify so no need to be
1463 # per jid
1464 self.show_bigger_avatar_timeout_id = None
1465 self.bigger_avatar_window = None
1466 self.show_avatar()
1468 # chatstate timers and state
1469 self.reset_kbd_mouse_timeout_vars()
1470 self._schedule_activity_timers()
1472 # Hook up signals
1473 id_ = self.parent_win.window.connect('motion-notify-event',
1474 self._on_window_motion_notify)
1475 self.handlers[id_] = self.parent_win.window
1476 message_tv_buffer = self.msg_textview.get_buffer()
1477 id_ = message_tv_buffer.connect('changed',
1478 self._on_message_tv_buffer_changed)
1479 self.handlers[id_] = message_tv_buffer
1481 widget = self.xml.get_object('avatar_eventbox')
1482 widget.set_property('height-request', gajim.config.get(
1483 'chat_avatar_height'))
1484 id_ = widget.connect('enter-notify-event',
1485 self.on_avatar_eventbox_enter_notify_event)
1486 self.handlers[id_] = widget
1488 id_ = widget.connect('leave-notify-event',
1489 self.on_avatar_eventbox_leave_notify_event)
1490 self.handlers[id_] = widget
1492 id_ = widget.connect('button-press-event',
1493 self.on_avatar_eventbox_button_press_event)
1494 self.handlers[id_] = widget
1496 widget = self.xml.get_object('location_eventbox')
1497 id_ = widget.connect('button-release-event',
1498 self.on_location_eventbox_button_release_event)
1499 self.handlers[id_] = widget
1501 for key in ('1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'):
1502 widget = self.xml.get_object(key + '_button')
1503 id_ = widget.connect('pressed', self.on_num_button_pressed, key)
1504 self.handlers[id_] = widget
1505 id_ = widget.connect('released', self.on_num_button_released)
1506 self.handlers[id_] = widget
1508 self.dtmf_window = self.xml.get_object('dtmf_window')
1509 id_ = self.dtmf_window.connect('focus-out-event',
1510 self.on_dtmf_window_focus_out_event)
1511 self.handlers[id_] = self.dtmf_window
1513 widget = self.xml.get_object('dtmf_button')
1514 id_ = widget.connect('clicked', self.on_dtmf_button_clicked)
1515 self.handlers[id_] = widget
1517 widget = self.xml.get_object('mic_hscale')
1518 id_ = widget.connect('value_changed', self.on_mic_hscale_value_changed)
1519 self.handlers[id_] = widget
1521 widget = self.xml.get_object('sound_hscale')
1522 id_ = widget.connect('value_changed', self.on_sound_hscale_value_changed)
1523 self.handlers[id_] = widget
1525 if not session:
1526 # Don't use previous session if we want to a specific resource
1527 # and it's not the same
1528 if not resource:
1529 resource = contact.resource
1530 session = gajim.connections[self.account].find_controlless_session(
1531 self.contact.jid, resource)
1533 self.setup_seclabel(self.xml.get_object('label_selector'))
1534 if session:
1535 session.control = self
1536 self.session = session
1538 if session.enable_encryption:
1539 self.print_esession_details()
1541 # Enable encryption if needed
1542 self.no_autonegotiation = False
1543 e2e_is_active = self.session and self.session.enable_encryption
1544 gpg_pref = gajim.config.get_per('contacts', contact.jid,
1545 'gpg_enabled')
1547 # try GPG first
1548 if not e2e_is_active and gpg_pref and \
1549 gajim.config.get_per('accounts', self.account, 'keyid') and \
1550 gajim.connections[self.account].USE_GPG:
1551 self.gpg_is_active = True
1552 gajim.encrypted_chats[self.account].append(contact.jid)
1553 msg = _('GPG encryption enabled')
1554 ChatControlBase.print_conversation_line(self, msg,
1555 'status', '', None)
1557 if self.session:
1558 self.session.loggable = gajim.config.get_per('accounts',
1559 self.account, 'log_encrypted_sessions')
1560 # GPG is always authenticated as we use GPG's WoT
1561 self._show_lock_image(self.gpg_is_active, 'GPG', self.gpg_is_active,
1562 self.session and self.session.is_loggable(), True)
1564 self.update_ui()
1565 # restore previous conversation
1566 self.restore_conversation()
1567 self.msg_textview.grab_focus()
1569 # PluginSystem: adding GUI extension point for this ChatControl
1570 # instance object
1571 gajim.plugin_manager.gui_extension_point('chat_control', self)
1573 def _update_toolbar(self):
1574 # Formatting
1575 if self.contact.supports(NS_XHTML_IM) and not self.gpg_is_active:
1576 self._formattings_button.set_sensitive(True)
1577 else:
1578 self._formattings_button.set_sensitive(False)
1580 # Add to roster
1581 if not isinstance(self.contact, GC_Contact) \
1582 and _('Not in Roster') in self.contact.groups:
1583 self._add_to_roster_button.show()
1584 else:
1585 self._add_to_roster_button.hide()
1587 # Jingle detection
1588 if self.contact.supports(NS_JINGLE_ICE_UDP) and \
1589 gajim.HAVE_FARSIGHT and self.contact.resource:
1590 self.audio_available = self.contact.supports(NS_JINGLE_RTP_AUDIO)
1591 self.video_available = self.contact.supports(NS_JINGLE_RTP_VIDEO)
1592 else:
1593 if self.video_available or self.audio_available:
1594 self.stop_jingle()
1595 self.video_available = False
1596 self.audio_available = False
1598 # Audio buttons
1599 self._audio_button.set_sensitive(self.audio_available)
1601 # Video buttons
1602 self._video_button.set_sensitive(self.video_available)
1604 # Send file
1605 if self.contact.supports(NS_FILE) and self.contact.resource:
1606 self._send_file_button.set_sensitive(True)
1607 self._send_file_button.set_tooltip_text('')
1608 else:
1609 self._send_file_button.set_sensitive(False)
1610 if not self.contact.supports(NS_FILE):
1611 self._send_file_button.set_tooltip_text(_(
1612 "This contact does not support file transfer."))
1613 else:
1614 self._send_file_button.set_tooltip_text(
1615 _("You need to know the real JID of the contact to send him or "
1616 "her a file."))
1618 # Convert to GC
1619 if self.contact.supports(NS_MUC):
1620 self._convert_to_gc_button.set_sensitive(True)
1621 else:
1622 self._convert_to_gc_button.set_sensitive(False)
1624 def update_all_pep_types(self):
1625 for pep_type in self._pep_images:
1626 self.update_pep(pep_type)
1628 def update_pep(self, pep_type):
1629 if isinstance(self.contact, GC_Contact):
1630 return
1631 if pep_type not in self._pep_images:
1632 return
1633 pep = self.contact.pep
1634 img = self._pep_images[pep_type]
1635 if pep_type in pep:
1636 img.set_from_pixbuf(pep[pep_type].asPixbufIcon())
1637 img.set_tooltip_markup(pep[pep_type].asMarkupText())
1638 img.show()
1639 else:
1640 img.hide()
1642 def _update_jingle(self, jingle_type):
1643 if jingle_type not in ('audio', 'video'):
1644 return
1645 banner_image = getattr(self, '_' + jingle_type + '_banner_image')
1646 state = getattr(self, jingle_type + '_state')
1647 if state == self.JINGLE_STATE_NULL:
1648 banner_image.hide()
1649 else:
1650 banner_image.show()
1651 if state == self.JINGLE_STATE_CONNECTING:
1652 banner_image.set_from_stock(
1653 gtk.STOCK_CONVERT, 1)
1654 elif state == self.JINGLE_STATE_CONNECTION_RECEIVED:
1655 banner_image.set_from_stock(
1656 gtk.STOCK_NETWORK, 1)
1657 elif state == self.JINGLE_STATE_CONNECTED:
1658 banner_image.set_from_stock(
1659 gtk.STOCK_CONNECT, 1)
1660 elif state == self.JINGLE_STATE_ERROR:
1661 banner_image.set_from_stock(
1662 gtk.STOCK_DIALOG_WARNING, 1)
1663 self.update_toolbar()
1665 def update_audio(self):
1666 self._update_jingle('audio')
1667 hbox = self.xml.get_object('audio_buttons_hbox')
1668 if self.audio_state == self.JINGLE_STATE_CONNECTED:
1669 # Set volume from config
1670 input_vol = gajim.config.get('audio_input_volume')
1671 output_vol = gajim.config.get('audio_output_volume')
1672 input_vol = max(min(input_vol, 100), 0)
1673 output_vol = max(min(output_vol, 100), 0)
1674 self.xml.get_object('mic_hscale').set_value(input_vol)
1675 self.xml.get_object('sound_hscale').set_value(output_vol)
1676 # Show vbox
1677 hbox.set_no_show_all(False)
1678 hbox.show_all()
1679 elif not self.audio_sid:
1680 hbox.set_no_show_all(True)
1681 hbox.hide()
1683 def update_video(self):
1684 self._update_jingle('video')
1686 def change_resource(self, resource):
1687 old_full_jid = self.get_full_jid()
1688 self.resource = resource
1689 new_full_jid = self.get_full_jid()
1690 # update gajim.last_message_time
1691 if old_full_jid in gajim.last_message_time[self.account]:
1692 gajim.last_message_time[self.account][new_full_jid] = \
1693 gajim.last_message_time[self.account][old_full_jid]
1694 # update events
1695 gajim.events.change_jid(self.account, old_full_jid, new_full_jid)
1696 # update MessageWindow._controls
1697 self.parent_win.change_jid(self.account, old_full_jid, new_full_jid)
1699 def stop_jingle(self, sid=None, reason=None):
1700 if self.audio_sid and sid in (self.audio_sid, None):
1701 self.close_jingle_content('audio')
1702 if self.video_sid and sid in (self.video_sid, None):
1703 self.close_jingle_content('video')
1706 def _set_jingle_state(self, jingle_type, state, sid=None, reason=None):
1707 if jingle_type not in ('audio', 'video'):
1708 return
1709 if state in ('connecting', 'connected', 'stop', 'error') and reason:
1710 str = _('%(type)s state : %(state)s, reason: %(reason)s') % {
1711 'type': jingle_type.capitalize(), 'state': state, 'reason': reason}
1712 self.print_conversation(str, 'info')
1714 states = {'connecting': self.JINGLE_STATE_CONNECTING,
1715 'connection_received': self.JINGLE_STATE_CONNECTION_RECEIVED,
1716 'connected': self.JINGLE_STATE_CONNECTED,
1717 'stop': self.JINGLE_STATE_NULL,
1718 'error': self.JINGLE_STATE_ERROR}
1720 jingle_state = states[state]
1721 if getattr(self, jingle_type + '_state') == jingle_state or state == 'error':
1722 return
1724 if state == 'stop' and getattr(self, jingle_type + '_sid') not in (None, sid):
1725 return
1727 setattr(self, jingle_type + '_state', jingle_state)
1729 if jingle_state == self.JINGLE_STATE_NULL:
1730 setattr(self, jingle_type + '_sid', None)
1731 if state in ('connection_received', 'connecting'):
1732 setattr(self, jingle_type + '_sid', sid)
1734 getattr(self, '_' + jingle_type + '_button').set_active(jingle_state != self.JINGLE_STATE_NULL)
1736 getattr(self, 'update_' + jingle_type)()
1738 def set_audio_state(self, state, sid=None, reason=None):
1739 self._set_jingle_state('audio', state, sid=sid, reason=reason)
1741 def set_video_state(self, state, sid=None, reason=None):
1742 self._set_jingle_state('video', state, sid=sid, reason=reason)
1744 def _get_audio_content(self):
1745 session = gajim.connections[self.account].get_jingle_session(
1746 self.contact.get_full_jid(), self.audio_sid)
1747 return session.get_content('audio')
1749 def on_num_button_pressed(self, widget, num):
1750 self._get_audio_content()._start_dtmf(num)
1752 def on_num_button_released(self, released):
1753 self._get_audio_content()._stop_dtmf()
1755 def on_dtmf_button_clicked(self, widget):
1756 self.dtmf_window.show_all()
1758 def on_dtmf_window_focus_out_event(self, widget, event):
1759 self.dtmf_window.hide()
1761 def on_mic_hscale_value_changed(self, widget, value):
1762 self._get_audio_content().set_mic_volume(value / 100)
1763 # Save volume to config
1764 gajim.config.set('audio_input_volume', value)
1767 def on_sound_hscale_value_changed(self, widget, value):
1768 self._get_audio_content().set_out_volume(value / 100)
1769 # Save volume to config
1770 gajim.config.set('audio_output_volume', value)
1772 def on_avatar_eventbox_enter_notify_event(self, widget, event):
1774 Enter the eventbox area so we under conditions add a timeout to show a
1775 bigger avatar after 0.5 sec
1777 jid = self.contact.jid
1778 avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid)
1779 if avatar_pixbuf in ('ask', None):
1780 return
1781 avatar_w = avatar_pixbuf.get_width()
1782 avatar_h = avatar_pixbuf.get_height()
1784 scaled_buf = self.xml.get_object('avatar_image').get_pixbuf()
1785 scaled_buf_w = scaled_buf.get_width()
1786 scaled_buf_h = scaled_buf.get_height()
1788 # do we have something bigger to show?
1789 if avatar_w > scaled_buf_w or avatar_h > scaled_buf_h:
1790 # wait for 0.5 sec in case we leave earlier
1791 if self.show_bigger_avatar_timeout_id is not None:
1792 gobject.source_remove(self.show_bigger_avatar_timeout_id)
1793 self.show_bigger_avatar_timeout_id = gobject.timeout_add(500,
1794 self.show_bigger_avatar, widget)
1796 def on_avatar_eventbox_leave_notify_event(self, widget, event):
1798 Left the eventbox area that holds the avatar img
1800 # did we add a timeout? if yes remove it
1801 if self.show_bigger_avatar_timeout_id is not None:
1802 gobject.source_remove(self.show_bigger_avatar_timeout_id)
1803 self.show_bigger_avatar_timeout_id = None
1805 def on_avatar_eventbox_button_press_event(self, widget, event):
1807 If right-clicked, show popup
1809 if event.button == 3: # right click
1810 menu = gtk.Menu()
1811 menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS)
1812 id_ = menuitem.connect('activate',
1813 gtkgui_helpers.on_avatar_save_as_menuitem_activate,
1814 self.contact.jid, self.contact.get_shown_name())
1815 self.handlers[id_] = menuitem
1816 menu.append(menuitem)
1817 menu.show_all()
1818 menu.connect('selection-done', lambda w:w.destroy())
1819 # show the menu
1820 menu.show_all()
1821 menu.popup(None, None, None, event.button, event.time)
1822 return True
1824 def on_location_eventbox_button_release_event(self, widget, event):
1825 if 'location' in self.contact.pep:
1826 location = self.contact.pep['location']._pep_specific_data
1827 if ('lat' in location) and ('lon' in location):
1828 uri = 'http://www.openstreetmap.org/?' + \
1829 'mlat=%(lat)s&mlon=%(lon)s&zoom=16' % {'lat': location['lat'],
1830 'lon': location['lon']}
1831 helpers.launch_browser_mailer('url', uri)
1833 def _on_window_motion_notify(self, widget, event):
1835 It gets called no matter if it is the active window or not
1837 if self.parent_win.get_active_jid() == self.contact.jid:
1838 # if window is the active one, change vars assisting chatstate
1839 self.mouse_over_in_last_5_secs = True
1840 self.mouse_over_in_last_30_secs = True
1842 def _schedule_activity_timers(self):
1843 self.possible_paused_timeout_id = gobject.timeout_add_seconds(5,
1844 self.check_for_possible_paused_chatstate, None)
1845 self.possible_inactive_timeout_id = gobject.timeout_add_seconds(30,
1846 self.check_for_possible_inactive_chatstate, None)
1848 def update_ui(self):
1849 # The name banner is drawn here
1850 ChatControlBase.update_ui(self)
1851 self.update_toolbar()
1853 def _update_banner_state_image(self):
1854 contact = gajim.contacts.get_contact_with_highest_priority(self.account,
1855 self.contact.jid)
1856 if not contact or self.resource:
1857 # For transient contacts
1858 contact = self.contact
1859 show = contact.show
1860 jid = contact.jid
1862 # Set banner image
1863 img_32 = gajim.interface.roster.get_appropriate_state_images(jid,
1864 size = '32', icon_name = show)
1865 img_16 = gajim.interface.roster.get_appropriate_state_images(jid,
1866 icon_name = show)
1867 if show in img_32 and img_32[show].get_pixbuf():
1868 # we have 32x32! use it!
1869 banner_image = img_32[show]
1870 use_size_32 = True
1871 else:
1872 banner_image = img_16[show]
1873 use_size_32 = False
1875 banner_status_img = self.xml.get_object('banner_status_image')
1876 if banner_image.get_storage_type() == gtk.IMAGE_ANIMATION:
1877 banner_status_img.set_from_animation(banner_image.get_animation())
1878 else:
1879 pix = banner_image.get_pixbuf()
1880 if pix is not None:
1881 if use_size_32:
1882 banner_status_img.set_from_pixbuf(pix)
1883 else: # we need to scale 16x16 to 32x32
1884 scaled_pix = pix.scale_simple(32, 32,
1885 gtk.gdk.INTERP_BILINEAR)
1886 banner_status_img.set_from_pixbuf(scaled_pix)
1888 def draw_banner_text(self):
1890 Draw the text in the fat line at the top of the window that houses the
1891 name, jid
1893 contact = self.contact
1894 jid = contact.jid
1896 banner_name_label = self.xml.get_object('banner_name_label')
1898 name = contact.get_shown_name()
1899 if self.resource:
1900 name += '/' + self.resource
1901 if self.TYPE_ID == message_control.TYPE_PM:
1902 name = _('%(nickname)s from group chat %(room_name)s') %\
1903 {'nickname': name, 'room_name': self.room_name}
1904 name = gobject.markup_escape_text(name)
1906 # We know our contacts nick, but if another contact has the same nick
1907 # in another account we need to also display the account.
1908 # except if we are talking to two different resources of the same contact
1909 acct_info = ''
1910 for account in gajim.contacts.get_accounts():
1911 if account == self.account:
1912 continue
1913 if acct_info: # We already found a contact with same nick
1914 break
1915 for jid in gajim.contacts.get_jid_list(account):
1916 other_contact_ = \
1917 gajim.contacts.get_first_contact_from_jid(account, jid)
1918 if other_contact_.get_shown_name() == self.contact.get_shown_name():
1919 acct_info = ' (%s)' % \
1920 gobject.markup_escape_text(self.account)
1921 break
1923 status = contact.status
1924 if status is not None:
1925 banner_name_label.set_ellipsize(pango.ELLIPSIZE_END)
1926 self.banner_status_label.set_ellipsize(pango.ELLIPSIZE_END)
1927 status_reduced = helpers.reduce_chars_newlines(status, max_lines = 1)
1928 else:
1929 status_reduced = ''
1930 status_escaped = gobject.markup_escape_text(status_reduced)
1932 font_attrs, font_attrs_small = self.get_font_attrs()
1933 st = gajim.config.get('displayed_chat_state_notifications')
1934 cs = contact.chatstate
1935 if cs and st in ('composing_only', 'all'):
1936 if contact.show == 'offline':
1937 chatstate = ''
1938 elif contact.composing_xep == 'XEP-0085':
1939 if st == 'all' or cs == 'composing':
1940 chatstate = helpers.get_uf_chatstate(cs)
1941 else:
1942 chatstate = ''
1943 elif contact.composing_xep == 'XEP-0022':
1944 if cs in ('composing', 'paused'):
1945 # only print composing, paused
1946 chatstate = helpers.get_uf_chatstate(cs)
1947 else:
1948 chatstate = ''
1949 else:
1950 # When does that happen ? See [7797] and [7804]
1951 chatstate = helpers.get_uf_chatstate(cs)
1953 label_text = '<span %s>%s</span><span %s>%s %s</span>' \
1954 % (font_attrs, name, font_attrs_small,
1955 acct_info, chatstate)
1956 if acct_info:
1957 acct_info = ' ' + acct_info
1958 label_tooltip = '%s%s %s' % (name, acct_info, chatstate)
1959 else:
1960 # weight="heavy" size="x-large"
1961 label_text = '<span %s>%s</span><span %s>%s</span>' % \
1962 (font_attrs, name, font_attrs_small, acct_info)
1963 if acct_info:
1964 acct_info = ' ' + acct_info
1965 label_tooltip = '%s%s' % (name, acct_info)
1967 if status_escaped:
1968 status_text = self.urlfinder.sub(self.make_href, status_escaped)
1969 status_text = '<span %s>%s</span>' % (font_attrs_small, status_escaped)
1970 self.banner_status_label.set_tooltip_text(status)
1971 self.banner_status_label.set_no_show_all(False)
1972 self.banner_status_label.show()
1973 else:
1974 status_text = ''
1975 self.banner_status_label.hide()
1976 self.banner_status_label.set_no_show_all(True)
1978 self.banner_status_label.set_markup(status_text)
1979 # setup the label that holds name and jid
1980 banner_name_label.set_markup(label_text)
1981 banner_name_label.set_tooltip_text(label_tooltip)
1983 def close_jingle_content(self, jingle_type):
1984 sid = getattr(self, jingle_type + '_sid')
1985 if not sid:
1986 return
1987 setattr(self, jingle_type + '_sid', None)
1988 setattr(self, jingle_type + '_state', self.JINGLE_STATE_NULL)
1989 session = gajim.connections[self.account].get_jingle_session(
1990 self.contact.get_full_jid(), sid)
1991 if session:
1992 content = session.get_content(jingle_type)
1993 if content:
1994 session.remove_content(content.creator, content.name)
1995 getattr(self, '_' + jingle_type + '_button').set_active(False)
1996 getattr(self, 'update_' + jingle_type)()
1998 def on_jingle_button_toggled(self, widget, jingle_type):
1999 img_name = 'gajim-%s_%s' % ({'audio': 'mic', 'video': 'cam'}[jingle_type],
2000 {True: 'active', False: 'inactive'}[widget.get_active()])
2001 path_to_img = gtkgui_helpers.get_icon_path(img_name)
2003 if widget.get_active():
2004 if getattr(self, jingle_type + '_state') == \
2005 self.JINGLE_STATE_NULL:
2006 sid = getattr(gajim.connections[self.account],
2007 'start_' + jingle_type)(self.contact.get_full_jid())
2008 getattr(self, 'set_' + jingle_type + '_state')('connecting', sid)
2009 else:
2010 self.close_jingle_content(jingle_type)
2012 img = getattr(self, '_' + jingle_type + '_button').get_property('image')
2013 img.set_from_file(path_to_img)
2015 def on_audio_button_toggled(self, widget):
2016 self.on_jingle_button_toggled(widget, 'audio')
2018 def on_video_button_toggled(self, widget):
2019 self.on_jingle_button_toggled(widget, 'video')
2021 def _toggle_gpg(self):
2022 if not self.gpg_is_active and not self.contact.keyID:
2023 dialogs.ErrorDialog(_('No GPG key assigned'),
2024 _('No GPG key is assigned to this contact. So you cannot '
2025 'encrypt messages with GPG.'))
2026 return
2027 ec = gajim.encrypted_chats[self.account]
2028 if self.gpg_is_active:
2029 # Disable encryption
2030 ec.remove(self.contact.jid)
2031 self.gpg_is_active = False
2032 loggable = False
2033 msg = _('GPG encryption disabled')
2034 ChatControlBase.print_conversation_line(self, msg,
2035 'status', '', None)
2036 if self.session:
2037 self.session.loggable = True
2039 else:
2040 # Enable encryption
2041 ec.append(self.contact.jid)
2042 self.gpg_is_active = True
2043 msg = _('GPG encryption enabled')
2044 ChatControlBase.print_conversation_line(self, msg,
2045 'status', '', None)
2047 loggable = gajim.config.get_per('accounts', self.account,
2048 'log_encrypted_sessions')
2050 if self.session:
2051 self.session.loggable = loggable
2053 loggable = self.session.is_loggable()
2054 else:
2055 loggable = loggable and gajim.config.should_log(self.account,
2056 self.contact.jid)
2058 if loggable:
2059 msg = _('Session WILL be logged')
2060 else:
2061 msg = _('Session WILL NOT be logged')
2063 ChatControlBase.print_conversation_line(self, msg,
2064 'status', '', None)
2066 gajim.config.set_per('contacts', self.contact.jid,
2067 'gpg_enabled', self.gpg_is_active)
2069 self._show_lock_image(self.gpg_is_active, 'GPG',
2070 self.gpg_is_active, loggable, True)
2072 def _show_lock_image(self, visible, enc_type = '', enc_enabled = False,
2073 chat_logged = False, authenticated = False):
2075 Set lock icon visibility and create tooltip
2077 #encryption %s active
2078 status_string = enc_enabled and _('is') or _('is NOT')
2079 #chat session %s be logged
2080 logged_string = chat_logged and _('will') or _('will NOT')
2082 if authenticated:
2083 #About encrypted chat session
2084 authenticated_string = _('and authenticated')
2085 img_path = gtkgui_helpers.get_icon_path('gajim-security_high')
2086 else:
2087 #About encrypted chat session
2088 authenticated_string = _('and NOT authenticated')
2089 img_path = gtkgui_helpers.get_icon_path('gajim-security_low')
2090 self.lock_image.set_from_file(img_path)
2092 #status will become 'is' or 'is not', authentificaed will become
2093 #'and authentificated' or 'and not authentificated', logged will become
2094 #'will' or 'will not'
2095 tooltip = _('%(type)s encryption %(status)s active %(authenticated)s.\n'
2096 'Your chat session %(logged)s be logged.') % {'type': enc_type,
2097 'status': status_string, 'authenticated': authenticated_string,
2098 'logged': logged_string}
2100 self.authentication_button.set_tooltip_text(tooltip)
2101 self.widget_set_visible(self.authentication_button, not visible)
2102 self.lock_image.set_sensitive(enc_enabled)
2104 def _on_authentication_button_clicked(self, widget):
2105 if self.gpg_is_active:
2106 dialogs.GPGInfoWindow(self)
2107 elif self.session and self.session.enable_encryption:
2108 dialogs.ESessionInfoWindow(self.session)
2110 def send_message(self, message, keyID='', chatstate=None, xhtml=None,
2111 process_commands=True):
2113 Send a message to contact
2115 if message in ('', None, '\n'):
2116 return None
2118 # refresh timers
2119 self.reset_kbd_mouse_timeout_vars()
2121 contact = self.contact
2123 encrypted = bool(self.session) and self.session.enable_encryption
2125 keyID = ''
2126 if self.gpg_is_active:
2127 keyID = contact.keyID
2128 encrypted = True
2129 if not keyID:
2130 keyID = 'UNKNOWN'
2132 chatstates_on = gajim.config.get('outgoing_chat_state_notifications') != \
2133 'disabled'
2134 composing_xep = contact.composing_xep
2135 chatstate_to_send = None
2136 if chatstates_on and contact is not None:
2137 if composing_xep is None:
2138 # no info about peer
2139 # send active to discover chat state capabilities
2140 # this is here (and not in send_chatstate)
2141 # because we want it sent with REAL message
2142 # (not standlone) eg. one that has body
2144 if contact.our_chatstate:
2145 # We already asked for xep 85, don't ask it twice
2146 composing_xep = 'asked_once'
2148 chatstate_to_send = 'active'
2149 contact.our_chatstate = 'ask' # pseudo state
2150 # if peer supports jep85 and we are not 'ask', send 'active'
2151 # NOTE: first active and 'ask' is set in gajim.py
2152 elif composing_xep is not False:
2153 # send active chatstate on every message (as XEP says)
2154 chatstate_to_send = 'active'
2155 contact.our_chatstate = 'active'
2157 gobject.source_remove(self.possible_paused_timeout_id)
2158 gobject.source_remove(self.possible_inactive_timeout_id)
2159 self._schedule_activity_timers()
2161 def _on_sent(id_, contact, message, encrypted, xhtml, label):
2162 if contact.supports(NS_RECEIPTS) and gajim.config.get_per('accounts',
2163 self.account, 'request_receipt'):
2164 xep0184_id = id_
2165 else:
2166 xep0184_id = None
2167 if label:
2168 displaymarking = label.getTag('displaymarking')
2169 else:
2170 displaymarking = None
2171 self.print_conversation(message, self.contact.jid, encrypted=encrypted,
2172 xep0184_id=xep0184_id, xhtml=xhtml, displaymarking=displaymarking)
2174 ChatControlBase.send_message(self, message, keyID, type_='chat',
2175 chatstate=chatstate_to_send, composing_xep=composing_xep,
2176 xhtml=xhtml, callback=_on_sent,
2177 callback_args=[contact, message, encrypted, xhtml, self.get_seclabel()],
2178 process_commands=process_commands)
2180 def check_for_possible_paused_chatstate(self, arg):
2182 Did we move mouse of that window or write something in message textview
2183 in the last 5 seconds? If yes - we go active for mouse, composing for
2184 kbd. If not - we go paused if we were previously composing
2186 contact = self.contact
2187 jid = contact.jid
2188 current_state = contact.our_chatstate
2189 if current_state is False: # jid doesn't support chatstates
2190 return False # stop looping
2192 message_buffer = self.msg_textview.get_buffer()
2193 if self.kbd_activity_in_last_5_secs and message_buffer.get_char_count():
2194 # Only composing if the keyboard activity was in text entry
2195 self.send_chatstate('composing')
2196 elif self.mouse_over_in_last_5_secs and\
2197 jid == self.parent_win.get_active_jid():
2198 self.send_chatstate('active')
2199 else:
2200 if current_state == 'composing':
2201 self.send_chatstate('paused') # pause composing
2203 # assume no activity and let the motion-notify or 'insert-text' make them
2204 # True refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds!
2205 self.reset_kbd_mouse_timeout_vars()
2206 return True # loop forever
2208 def check_for_possible_inactive_chatstate(self, arg):
2210 Did we move mouse over that window or wrote something in message textview
2211 in the last 30 seconds? if yes - we go active. If no - we go inactive
2213 contact = self.contact
2215 current_state = contact.our_chatstate
2216 if current_state is False: # jid doesn't support chatstates
2217 return False # stop looping
2219 if self.mouse_over_in_last_5_secs or self.kbd_activity_in_last_5_secs:
2220 return True # loop forever
2222 if not self.mouse_over_in_last_30_secs or \
2223 self.kbd_activity_in_last_30_secs:
2224 self.send_chatstate('inactive', contact)
2226 # assume no activity and let the motion-notify or 'insert-text' make them
2227 # True refresh 30 seconds too or else it's 30 - 5 = 25 seconds!
2228 self.reset_kbd_mouse_timeout_vars()
2229 return True # loop forever
2231 def reset_kbd_mouse_timeout_vars(self):
2232 self.kbd_activity_in_last_5_secs = False
2233 self.mouse_over_in_last_5_secs = False
2234 self.mouse_over_in_last_30_secs = False
2235 self.kbd_activity_in_last_30_secs = False
2237 def on_cancel_session_negotiation(self):
2238 msg = _('Session negotiation cancelled')
2239 ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
2241 def print_archiving_session_details(self):
2243 Print esession settings to textview
2245 archiving = bool(self.session) and isinstance(self.session,
2246 ArchivingStanzaSession) and self.session.archiving
2247 if archiving:
2248 msg = _('This session WILL be archived on server')
2249 else:
2250 msg = _('This session WILL NOT be archived on server')
2251 ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
2253 def print_esession_details(self):
2255 Print esession settings to textview
2257 e2e_is_active = bool(self.session) and self.session.enable_encryption
2258 if e2e_is_active:
2259 msg = _('This session is encrypted')
2261 if self.session.is_loggable():
2262 msg += _(' and WILL be logged')
2263 else:
2264 msg += _(' and WILL NOT be logged')
2266 ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
2268 if not self.session.verified_identity:
2269 ChatControlBase.print_conversation_line(self, _("Remote contact's identity not verified. Click the shield button for more details."), 'status', '', None)
2270 else:
2271 msg = _('E2E encryption disabled')
2272 ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
2274 self._show_lock_image(e2e_is_active, 'E2E', e2e_is_active, self.session and \
2275 self.session.is_loggable(), self.session and self.session.verified_identity)
2277 def print_session_details(self):
2278 if isinstance(self.session, EncryptedStanzaSession):
2279 self.print_esession_details()
2280 elif isinstance(self.session, ArchivingStanzaSession):
2281 self.print_archiving_session_details()
2283 def print_conversation(self, text, frm='', tim=None, encrypted=False,
2284 subject=None, xhtml=None, simple=False, xep0184_id=None,
2285 displaymarking=None):
2287 Print a line in the conversation
2289 If frm is set to status: it's a status message.
2290 if frm is set to error: it's an error message. The difference between
2291 status and error is mainly that with error, msg count as a new message
2292 (in systray and in control).
2293 If frm is set to info: it's a information message.
2294 If frm is set to print_queue: it is incomming from queue.
2295 If frm is set to another value: it's an outgoing message.
2296 If frm is not set: it's an incomming message.
2298 contact = self.contact
2300 if frm == 'status':
2301 if not gajim.config.get('print_status_in_chats'):
2302 return
2303 kind = 'status'
2304 name = ''
2305 elif frm == 'error':
2306 kind = 'error'
2307 name = ''
2308 elif frm == 'info':
2309 kind = 'info'
2310 name = ''
2311 else:
2312 if self.session and self.session.enable_encryption:
2313 # ESessions
2314 if not encrypted:
2315 msg = _('The following message was NOT encrypted')
2316 ChatControlBase.print_conversation_line(self, msg, 'status', '',
2317 tim)
2318 else:
2319 # GPG encryption
2320 if encrypted and not self.gpg_is_active:
2321 msg = _('The following message was encrypted')
2322 ChatControlBase.print_conversation_line(self, msg, 'status', '',
2323 tim)
2324 # turn on OpenPGP if this was in fact a XEP-0027 encrypted message
2325 if encrypted == 'xep27':
2326 self._toggle_gpg()
2327 elif not encrypted and self.gpg_is_active:
2328 msg = _('The following message was NOT encrypted')
2329 ChatControlBase.print_conversation_line(self, msg, 'status', '',
2330 tim)
2331 if not frm:
2332 kind = 'incoming'
2333 name = contact.get_shown_name()
2334 elif frm == 'print_queue': # incoming message, but do not update time
2335 kind = 'incoming_queue'
2336 name = contact.get_shown_name()
2337 else:
2338 kind = 'outgoing'
2339 name = gajim.nicks[self.account]
2340 if not xhtml and not (encrypted and self.gpg_is_active) and \
2341 gajim.config.get('rst_formatting_outgoing_messages'):
2342 from common.rst_xhtml_generator import create_xhtml
2343 xhtml = create_xhtml(text)
2344 if xhtml:
2345 xhtml = '<body xmlns="%s">%s</body>' % (NS_XHTML, xhtml)
2346 ChatControlBase.print_conversation_line(self, text, kind, name, tim,
2347 subject=subject, old_kind=self.old_msg_kind, xhtml=xhtml,
2348 simple=simple, xep0184_id=xep0184_id, displaymarking=displaymarking)
2349 if text.startswith('/me ') or text.startswith('/me\n'):
2350 self.old_msg_kind = None
2351 else:
2352 self.old_msg_kind = kind
2354 def get_tab_label(self, chatstate):
2355 unread = ''
2356 if self.resource:
2357 jid = self.contact.get_full_jid()
2358 else:
2359 jid = self.contact.jid
2360 num_unread = len(gajim.events.get_events(self.account, jid,
2361 ['printed_' + self.type_id, self.type_id]))
2362 if num_unread == 1 and not gajim.config.get('show_unread_tab_icon'):
2363 unread = '*'
2364 elif num_unread > 1:
2365 unread = '[' + unicode(num_unread) + ']'
2367 # Draw tab label using chatstate
2368 theme = gajim.config.get('roster_theme')
2369 color = None
2370 if not chatstate:
2371 chatstate = self.contact.chatstate
2372 if chatstate is not None:
2373 if chatstate == 'composing':
2374 color = gajim.config.get_per('themes', theme,
2375 'state_composing_color')
2376 elif chatstate == 'inactive':
2377 color = gajim.config.get_per('themes', theme,
2378 'state_inactive_color')
2379 elif chatstate == 'gone':
2380 color = gajim.config.get_per('themes', theme,
2381 'state_gone_color')
2382 elif chatstate == 'paused':
2383 color = gajim.config.get_per('themes', theme,
2384 'state_paused_color')
2385 if color:
2386 # We set the color for when it's the current tab or not
2387 color = gtk.gdk.colormap_get_system().alloc_color(color)
2388 # In inactive tab color to be lighter against the darker inactive
2389 # background
2390 if chatstate in ('inactive', 'gone') and\
2391 self.parent_win.get_active_control() != self:
2392 color = self.lighten_color(color)
2393 else: # active or not chatstate, get color from gtk
2394 color = self.parent_win.notebook.style.fg[gtk.STATE_ACTIVE]
2397 name = self.contact.get_shown_name()
2398 if self.resource:
2399 name += '/' + self.resource
2400 label_str = gobject.markup_escape_text(name)
2401 if num_unread: # if unread, text in the label becomes bold
2402 label_str = '<b>' + unread + label_str + '</b>'
2403 return (label_str, color)
2405 def get_tab_image(self, count_unread=True):
2406 if self.resource:
2407 jid = self.contact.get_full_jid()
2408 else:
2409 jid = self.contact.jid
2410 if count_unread:
2411 num_unread = len(gajim.events.get_events(self.account, jid,
2412 ['printed_' + self.type_id, self.type_id]))
2413 else:
2414 num_unread = 0
2415 # Set tab image (always 16x16); unread messages show the 'event' image
2416 tab_img = None
2418 if num_unread and gajim.config.get('show_unread_tab_icon'):
2419 img_16 = gajim.interface.roster.get_appropriate_state_images(
2420 self.contact.jid, icon_name = 'event')
2421 tab_img = img_16['event']
2422 else:
2423 contact = gajim.contacts.get_contact_with_highest_priority(
2424 self.account, self.contact.jid)
2425 if not contact or self.resource:
2426 # For transient contacts
2427 contact = self.contact
2428 img_16 = gajim.interface.roster.get_appropriate_state_images(
2429 self.contact.jid, icon_name=contact.show)
2430 tab_img = img_16[contact.show]
2432 return tab_img
2434 def prepare_context_menu(self, hide_buttonbar_items=False):
2436 Set compact view menuitem active state sets active and sensitivity state
2437 for toggle_gpg_menuitem sets sensitivity for history_menuitem (False for
2438 tranasports) and file_transfer_menuitem and hide()/show() for
2439 add_to_roster_menuitem
2441 menu = gui_menu_builder.get_contact_menu(self.contact, self.account,
2442 use_multiple_contacts=False, show_start_chat=False,
2443 show_encryption=True, control=self,
2444 show_buttonbar_items=not hide_buttonbar_items)
2445 return menu
2447 def send_chatstate(self, state, contact = None):
2449 Send OUR chatstate as STANDLONE chat state message (eg. no body)
2450 to contact only if new chatstate is different from the previous one
2451 if jid is not specified, send to active tab
2453 # JEP 85 does not allow resending the same chatstate
2454 # this function checks for that and just returns so it's safe to call it
2455 # with same state.
2457 # This functions also checks for violation in state transitions
2458 # and raises RuntimeException with appropriate message
2459 # more on that http://www.jabber.org/jeps/jep-0085.html#statechart
2461 # do not send nothing if we have chat state notifications disabled
2462 # that means we won't reply to the <active/> from other peer
2463 # so we do not broadcast jep85 capabalities
2464 chatstate_setting = gajim.config.get('outgoing_chat_state_notifications')
2465 if chatstate_setting == 'disabled':
2466 return
2467 elif chatstate_setting == 'composing_only' and state != 'active' and\
2468 state != 'composing':
2469 return
2471 if contact is None:
2472 contact = self.parent_win.get_active_contact()
2473 if contact is None:
2474 # contact was from pm in MUC, and left the room so contact is None
2475 # so we cannot send chatstate anymore
2476 return
2478 # Don't send chatstates to offline contacts
2479 if contact.show == 'offline':
2480 return
2482 if contact.composing_xep is False: # jid cannot do xep85 nor xep22
2483 return
2485 # if the new state we wanna send (state) equals
2486 # the current state (contact.our_chatstate) then return
2487 if contact.our_chatstate == state:
2488 return
2490 if contact.composing_xep is None:
2491 # we don't know anything about jid, so return
2492 # NOTE:
2493 # send 'active', set current state to 'ask' and return is done
2494 # in self.send_message() because we need REAL message (with <body>)
2495 # for that procedure so return to make sure we send only once
2496 # 'active' until we know peer supports jep85
2497 return
2499 if contact.our_chatstate == 'ask':
2500 return
2502 # in JEP22, when we already sent stop composing
2503 # notification on paused, don't resend it
2504 if contact.composing_xep == 'XEP-0022' and \
2505 contact.our_chatstate in ('paused', 'active', 'inactive') and \
2506 state is not 'composing': # not composing == in (active, inactive, gone)
2507 contact.our_chatstate = 'active'
2508 self.reset_kbd_mouse_timeout_vars()
2509 return
2511 # prevent going paused if we we were not composing (JEP violation)
2512 if state == 'paused' and not contact.our_chatstate == 'composing':
2513 # go active before
2514 MessageControl.send_message(self, None, chatstate = 'active')
2515 contact.our_chatstate = 'active'
2516 self.reset_kbd_mouse_timeout_vars()
2518 # if we're inactive prevent composing (JEP violation)
2519 elif contact.our_chatstate == 'inactive' and state == 'composing':
2520 # go active before
2521 MessageControl.send_message(self, None, chatstate = 'active')
2522 contact.our_chatstate = 'active'
2523 self.reset_kbd_mouse_timeout_vars()
2525 MessageControl.send_message(self, None, chatstate = state,
2526 msg_id = contact.msg_id, composing_xep = contact.composing_xep)
2527 contact.our_chatstate = state
2528 if contact.our_chatstate == 'active':
2529 self.reset_kbd_mouse_timeout_vars()
2531 def shutdown(self):
2532 # PluginSystem: calling shutdown of super class (ChatControlBase) to let it remove
2533 # it's GUI extension points
2534 super(ChatControl, self).shutdown()
2535 # PluginSystem: removing GUI extension points connected with ChatControl
2536 # instance object
2537 gajim.plugin_manager.remove_gui_extension_point('chat_control', self) # Send 'gone' chatstate
2539 self.send_chatstate('gone', self.contact)
2540 self.contact.chatstate = None
2541 self.contact.our_chatstate = None
2543 for jingle_type in ('audio', 'video'):
2544 self.close_jingle_content(jingle_type)
2546 # disconnect self from session
2547 if self.session:
2548 self.session.control = None
2550 # Disconnect timer callbacks
2551 gobject.source_remove(self.possible_paused_timeout_id)
2552 gobject.source_remove(self.possible_inactive_timeout_id)
2553 # Remove bigger avatar window
2554 if self.bigger_avatar_window:
2555 self.bigger_avatar_window.destroy()
2556 # Clean events
2557 gajim.events.remove_events(self.account, self.get_full_jid(),
2558 types = ['printed_' + self.type_id, self.type_id])
2559 # Remove contact instance if contact has been removed
2560 key = (self.contact.jid, self.account)
2561 roster = gajim.interface.roster
2562 if key in roster.contacts_to_be_removed.keys() and \
2563 not roster.contact_has_pending_roster_events(self.contact, self.account):
2564 backend = roster.contacts_to_be_removed[key]['backend']
2565 del roster.contacts_to_be_removed[key]
2566 roster.remove_contact(self.contact.jid, self.account, force=True,
2567 backend=backend)
2568 # remove all register handlers on widgets, created by self.xml
2569 # to prevent circular references among objects
2570 for i in self.handlers.keys():
2571 if self.handlers[i].handler_is_connected(i):
2572 self.handlers[i].disconnect(i)
2573 del self.handlers[i]
2574 self.conv_textview.del_handlers()
2575 if gajim.config.get('use_speller') and HAS_GTK_SPELL:
2576 spell_obj = gtkspell.get_from_text_view(self.msg_textview)
2577 if spell_obj:
2578 spell_obj.detach()
2579 self.msg_textview.destroy()
2581 def minimizable(self):
2582 return False
2584 def safe_shutdown(self):
2585 return False
2587 def allow_shutdown(self, method, on_yes, on_no, on_minimize):
2588 if time.time() - gajim.last_message_time[self.account]\
2589 [self.get_full_jid()] < 2:
2590 # 2 seconds
2591 def on_ok():
2592 on_yes(self)
2594 def on_cancel():
2595 on_no(self)
2597 dialogs.ConfirmationDialog(
2598 # %s is being replaced in the code with JID
2599 _('You just received a new message from "%s"') % self.contact.jid,
2600 _('If you close this tab and you have history disabled, '\
2601 'this message will be lost.'), on_response_ok=on_ok,
2602 on_response_cancel=on_cancel)
2603 return
2604 on_yes(self)
2606 def handle_incoming_chatstate(self):
2608 Handle incoming chatstate that jid SENT TO us
2610 self.draw_banner_text()
2611 # update chatstate in tab for this chat
2612 self.parent_win.redraw_tab(self, self.contact.chatstate)
2614 def set_control_active(self, state):
2615 ChatControlBase.set_control_active(self, state)
2616 # send chatstate inactive to the one we're leaving
2617 # and active to the one we visit
2618 if state:
2619 self.send_chatstate('active', self.contact)
2620 else:
2621 self.send_chatstate('inactive', self.contact)
2622 # Hide bigger avatar window
2623 if self.bigger_avatar_window:
2624 self.bigger_avatar_window.destroy()
2625 self.bigger_avatar_window = None
2626 # Re-show the small avatar
2627 self.show_avatar()
2629 def show_avatar(self):
2630 if not gajim.config.get('show_avatar_in_chat'):
2631 return
2633 jid_with_resource = self.contact.get_full_jid()
2634 pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid_with_resource)
2635 if pixbuf == 'ask':
2636 # we don't have the vcard
2637 if self.TYPE_ID == message_control.TYPE_PM:
2638 if self.gc_contact.jid:
2639 # We know the real jid of this contact
2640 real_jid = self.gc_contact.jid
2641 if self.gc_contact.resource:
2642 real_jid += '/' + self.gc_contact.resource
2643 else:
2644 real_jid = jid_with_resource
2645 gajim.connections[self.account].request_vcard(real_jid,
2646 jid_with_resource)
2647 else:
2648 gajim.connections[self.account].request_vcard(jid_with_resource)
2649 return
2650 elif pixbuf:
2651 scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'chat')
2652 else:
2653 scaled_pixbuf = None
2655 image = self.xml.get_object('avatar_image')
2656 image.set_from_pixbuf(scaled_pixbuf)
2657 image.show_all()
2659 def _on_drag_data_received(self, widget, context, x, y, selection,
2660 target_type, timestamp):
2661 if not selection.data:
2662 return
2663 if self.TYPE_ID == message_control.TYPE_PM:
2664 c = self.gc_contact
2665 else:
2666 c = self.contact
2667 if target_type == self.TARGET_TYPE_URI_LIST:
2668 if not c.resource: # If no resource is known, we can't send a file
2669 return
2670 uri = selection.data.strip()
2671 uri_splitted = uri.split() # we may have more than one file dropped
2672 for uri in uri_splitted:
2673 path = helpers.get_file_path_from_dnd_dropped_uri(uri)
2674 if os.path.isfile(path): # is it file?
2675 ft = gajim.interface.instances['file_transfers']
2676 ft.send_file(self.account, c, path)
2677 return
2679 # chat2muc
2680 treeview = gajim.interface.roster.tree
2681 model = treeview.get_model()
2682 data = selection.data
2683 path = treeview.get_selection().get_selected_rows()[1][0]
2684 iter_ = model.get_iter(path)
2685 type_ = model[iter_][2]
2686 if type_ != 'contact': # source is not a contact
2687 return
2688 dropped_jid = data.decode('utf-8')
2690 dropped_transport = gajim.get_transport_name_from_jid(dropped_jid)
2691 c_transport = gajim.get_transport_name_from_jid(c.jid)
2692 if dropped_transport or c_transport:
2693 return # transport contacts cannot be invited
2695 dialogs.TransformChatToMUC(self.account, [c.jid], [dropped_jid])
2697 def _on_message_tv_buffer_changed(self, textbuffer):
2698 self.kbd_activity_in_last_5_secs = True
2699 self.kbd_activity_in_last_30_secs = True
2700 if textbuffer.get_char_count():
2701 self.send_chatstate('composing', self.contact)
2703 e2e_is_active = self.session and \
2704 self.session.enable_encryption
2705 e2e_pref = gajim.config.get_per('accounts', self.account,
2706 'enable_esessions') and gajim.config.get_per('accounts',
2707 self.account, 'autonegotiate_esessions') and gajim.config.get_per(
2708 'contacts', self.contact.jid, 'autonegotiate_esessions')
2709 want_e2e = not e2e_is_active and not self.gpg_is_active \
2710 and e2e_pref
2712 if want_e2e and not self.no_autonegotiation \
2713 and gajim.HAVE_PYCRYPTO and self.contact.supports(NS_ESESSION):
2714 self.begin_e2e_negotiation()
2715 elif (not self.session or not self.session.status) and \
2716 gajim.connections[self.account].archiving_supported:
2717 self.begin_archiving_negotiation()
2718 else:
2719 self.send_chatstate('active', self.contact)
2721 def restore_conversation(self):
2722 jid = self.contact.jid
2723 # don't restore lines if it's a transport
2724 if gajim.jid_is_transport(jid):
2725 return
2727 # How many lines to restore and when to time them out
2728 restore_how_many = gajim.config.get('restore_lines')
2729 if restore_how_many <= 0:
2730 return
2731 timeout = gajim.config.get('restore_timeout') # in minutes
2733 # number of messages that are in queue and are already logged, we want
2734 # to avoid duplication
2735 pending_how_many = len(gajim.events.get_events(self.account, jid,
2736 ['chat', 'pm']))
2737 if self.resource:
2738 pending_how_many += len(gajim.events.get_events(self.account,
2739 self.contact.get_full_jid(), ['chat', 'pm']))
2741 try:
2742 rows = gajim.logger.get_last_conversation_lines(jid, restore_how_many,
2743 pending_how_many, timeout, self.account)
2744 except exceptions.DatabaseMalformed:
2745 import common.logger
2746 dialogs.ErrorDialog(_('Database Error'),
2747 _('The database file (%s) cannot be read. Try to repair it or remove it (all history will be lost).') % common.logger.LOG_DB_PATH)
2748 rows = []
2749 local_old_kind = None
2750 for row in rows: # row[0] time, row[1] has kind, row[2] the message
2751 if not row[2]: # message is empty, we don't print it
2752 continue
2753 if row[1] in (constants.KIND_CHAT_MSG_SENT,
2754 constants.KIND_SINGLE_MSG_SENT):
2755 kind = 'outgoing'
2756 name = gajim.nicks[self.account]
2757 elif row[1] in (constants.KIND_SINGLE_MSG_RECV,
2758 constants.KIND_CHAT_MSG_RECV):
2759 kind = 'incoming'
2760 name = self.contact.get_shown_name()
2761 elif row[1] == constants.KIND_ERROR:
2762 kind = 'status'
2763 name = self.contact.get_shown_name()
2765 tim = time.localtime(float(row[0]))
2767 if gajim.config.get('restored_messages_small'):
2768 small_attr = ['small']
2769 else:
2770 small_attr = []
2771 ChatControlBase.print_conversation_line(self, row[2], kind, name, tim,
2772 small_attr,
2773 small_attr + ['restored_message'],
2774 small_attr + ['restored_message'],
2775 False, old_kind = local_old_kind)
2776 if row[2].startswith('/me ') or row[2].startswith('/me\n'):
2777 local_old_kind = None
2778 else:
2779 local_old_kind = kind
2780 if len(rows):
2781 self.conv_textview.print_empty_line()
2783 def read_queue(self):
2785 Read queue and print messages containted in it
2787 jid = self.contact.jid
2788 jid_with_resource = jid
2789 if self.resource:
2790 jid_with_resource += '/' + self.resource
2791 events = gajim.events.get_events(self.account, jid_with_resource)
2793 # list of message ids which should be marked as read
2794 message_ids = []
2795 for event in events:
2796 if event.type_ != self.type_id:
2797 continue
2798 data = event.parameters
2799 kind = data[2]
2800 if kind == 'error':
2801 kind = 'info'
2802 else:
2803 kind = 'print_queue'
2804 dm = None
2805 if len(data) > 10:
2806 dm = data[10]
2807 self.print_conversation(data[0], kind, tim = data[3],
2808 encrypted = data[4], subject = data[1], xhtml = data[7],
2809 displaymarking=dm)
2810 if len(data) > 6 and isinstance(data[6], int):
2811 message_ids.append(data[6])
2813 if len(data) > 8:
2814 self.set_session(data[8])
2815 if message_ids:
2816 gajim.logger.set_read_messages(message_ids)
2817 gajim.events.remove_events(self.account, jid_with_resource,
2818 types = [self.type_id])
2820 typ = 'chat' # Is it a normal chat or a pm ?
2822 # reset to status image in gc if it is a pm
2823 # Is it a pm ?
2824 room_jid, nick = gajim.get_room_and_nick_from_fjid(jid)
2825 control = gajim.interface.msg_win_mgr.get_gc_control(room_jid,
2826 self.account)
2827 if control and control.type_id == message_control.TYPE_GC:
2828 control.update_ui()
2829 control.parent_win.show_title()
2830 typ = 'pm'
2832 self.redraw_after_event_removed(jid)
2833 if (self.contact.show in ('offline', 'error')):
2834 show_offline = gajim.config.get('showoffline')
2835 show_transports = gajim.config.get('show_transports_group')
2836 if (not show_transports and gajim.jid_is_transport(jid)) or \
2837 (not show_offline and typ == 'chat' and \
2838 len(gajim.contacts.get_contacts(self.account, jid)) < 2):
2839 gajim.interface.roster.remove_to_be_removed(self.contact.jid,
2840 self.account)
2841 elif typ == 'pm':
2842 control.remove_contact(nick)
2844 def show_bigger_avatar(self, small_avatar):
2846 Resize the avatar, if needed, so it has at max half the screen size and
2847 shows it
2849 if not small_avatar.window:
2850 # Tab has been closed since we hovered the avatar
2851 return
2852 avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(
2853 self.contact.jid)
2854 if avatar_pixbuf in ('ask', None):
2855 return
2856 # Hide the small avatar
2857 # this code hides the small avatar when we show a bigger one in case
2858 # the avatar has a transparency hole in the middle
2859 # so when we show the big one we avoid seeing the small one behind.
2860 # It's why I set it transparent.
2861 image = self.xml.get_object('avatar_image')
2862 pixbuf = image.get_pixbuf()
2863 pixbuf.fill(0xffffff00L) # RGBA
2864 image.queue_draw()
2866 screen_w = gtk.gdk.screen_width()
2867 screen_h = gtk.gdk.screen_height()
2868 avatar_w = avatar_pixbuf.get_width()
2869 avatar_h = avatar_pixbuf.get_height()
2870 half_scr_w = screen_w / 2
2871 half_scr_h = screen_h / 2
2872 if avatar_w > half_scr_w:
2873 avatar_w = half_scr_w
2874 if avatar_h > half_scr_h:
2875 avatar_h = half_scr_h
2876 window = gtk.Window(gtk.WINDOW_POPUP)
2877 self.bigger_avatar_window = window
2878 pixmap, mask = avatar_pixbuf.render_pixmap_and_mask()
2879 window.set_size_request(avatar_w, avatar_h)
2880 # we should make the cursor visible
2881 # gtk+ doesn't make use of the motion notify on gtkwindow by default
2882 # so this line adds that
2883 window.set_events(gtk.gdk.POINTER_MOTION_MASK)
2884 window.set_app_paintable(True)
2885 window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_TOOLTIP)
2887 window.realize()
2888 window.window.set_back_pixmap(pixmap, False) # make it transparent
2889 window.window.shape_combine_mask(mask, 0, 0)
2891 # make the bigger avatar window show up centered
2892 x0, y0 = small_avatar.window.get_origin()
2893 x0 += small_avatar.allocation.x
2894 y0 += small_avatar.allocation.y
2895 center_x= x0 + (small_avatar.allocation.width / 2)
2896 center_y = y0 + (small_avatar.allocation.height / 2)
2897 pos_x, pos_y = center_x - (avatar_w / 2), center_y - (avatar_h / 2)
2898 window.move(pos_x, pos_y)
2899 # make the cursor invisible so we can see the image
2900 invisible_cursor = gtkgui_helpers.get_invisible_cursor()
2901 window.window.set_cursor(invisible_cursor)
2903 # we should hide the window
2904 window.connect('leave_notify_event',
2905 self._on_window_avatar_leave_notify_event)
2906 window.connect('motion-notify-event',
2907 self._on_window_motion_notify_event)
2909 window.show_all()
2911 def _on_window_avatar_leave_notify_event(self, widget, event):
2913 Just left the popup window that holds avatar
2915 self.bigger_avatar_window.destroy()
2916 self.bigger_avatar_window = None
2917 # Re-show the small avatar
2918 self.show_avatar()
2920 def _on_window_motion_notify_event(self, widget, event):
2922 Just moved the mouse so show the cursor
2924 cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)
2925 self.bigger_avatar_window.window.set_cursor(cursor)
2927 def _on_send_file_menuitem_activate(self, widget):
2928 self._on_send_file()
2930 def _on_add_to_roster_menuitem_activate(self, widget):
2931 dialogs.AddNewContactWindow(self.account, self.contact.jid)
2933 def _on_contact_information_menuitem_activate(self, widget):
2934 gajim.interface.roster.on_info(widget, self.contact, self.account)
2936 def _on_toggle_gpg_menuitem_activate(self, widget):
2937 self._toggle_gpg()
2939 def _on_convert_to_gc_menuitem_activate(self, widget):
2941 User wants to invite some friends to chat
2943 dialogs.TransformChatToMUC(self.account, [self.contact.jid])
2945 def _on_toggle_e2e_menuitem_activate(self, widget):
2946 if self.session and self.session.enable_encryption:
2947 # e2e was enabled, disable it
2948 jid = str(self.session.jid)
2949 thread_id = self.session.thread_id
2951 self.session.terminate_e2e()
2953 gajim.connections[self.account].delete_session(jid, thread_id)
2955 # presumably the user had a good reason to shut it off, so
2956 # disable autonegotiation too
2957 self.no_autonegotiation = True
2958 else:
2959 self.begin_e2e_negotiation()
2961 def begin_negotiation(self):
2962 self.no_autonegotiation = True
2964 if not self.session:
2965 fjid = self.contact.get_full_jid()
2966 new_sess = gajim.connections[self.account].make_new_session(fjid, type_=self.type_id)
2967 self.set_session(new_sess)
2969 def begin_e2e_negotiation(self):
2970 self.begin_negotiation()
2971 self.session.negotiate_e2e(False)
2973 def begin_archiving_negotiation(self):
2974 self.begin_negotiation()
2975 self.session.negotiate_archiving()
2977 def got_connected(self):
2978 ChatControlBase.got_connected(self)
2979 # Refreshing contact
2980 contact = gajim.contacts.get_contact_with_highest_priority(
2981 self.account, self.contact.jid)
2982 if isinstance(contact, GC_Contact):
2983 contact = contact.as_contact()
2984 if contact:
2985 self.contact = contact
2986 self.draw_banner()
2988 def update_status_display(self, name, uf_show, status):
2990 Print the contact's status and update the status/GPG image
2992 self.update_ui()
2993 self.parent_win.redraw_tab(self)
2995 self.print_conversation(_('%(name)s is now %(status)s') % {'name': name,
2996 'status': uf_show}, 'status')
2998 if status:
2999 self.print_conversation(' (', 'status', simple=True)
3000 self.print_conversation('%s' % (status), 'status', simple=True)
3001 self.print_conversation(')', 'status', simple=True)