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