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