Module chat_control
[hide private]
[frames] | no frames]

Source Code for Module chat_control

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