don't traceback when we get disconnected wile we parse stream features. Fixes #5574
[gajim.git] / src / roster_window.py
blob0dd61cee9fdbc95a0deb3ea5ac2a201f9a4ddce9
1 # -*- coding: utf-8 -*-
2 ## src/roster_window.py
3 ##
4 ## Copyright (C) 2003-2008 Yann Leboulanger <asterix AT lagaule.org>
5 ## Copyright (C) 2005 Alex Mauer <hawke AT hawkesnest.net>
6 ## Stéphan Kochen <stephan AT kochen.nl>
7 ## Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
8 ## Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
9 ## Nikos Kouremenos <kourem AT gmail.com>
10 ## Copyright (C) 2006 Stefan Bethge <stefan AT lanpartei.de>
11 ## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
12 ## Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
13 ## James Newton <redshodan AT gmail.com>
14 ## Tomasz Melcer <liori AT exroot.org>
15 ## Julien Pivotto <roidelapluie AT gmail.com>
16 ## Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
17 ## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
18 ## Jonathan Schleifer <js-gajim AT webkeks.org>
20 ## This file is part of Gajim.
22 ## Gajim is free software; you can redistribute it and/or modify
23 ## it under the terms of the GNU General Public License as published
24 ## by the Free Software Foundation; version 3 only.
26 ## Gajim is distributed in the hope that it will be useful,
27 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
28 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 ## GNU General Public License for more details.
31 ## You should have received a copy of the GNU General Public License
32 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
35 import gtk
36 import pango
37 import gobject
38 import os
39 import sys
40 import time
41 import locale
43 import common.sleepy
44 import history_window
45 import dialogs
46 import vcard
47 import config
48 import disco
49 import gtkgui_helpers
50 import gui_menu_builder
51 import cell_renderer_image
52 import tooltips
53 import message_control
54 import adhoc_commands
55 import features_window
57 from common import gajim
58 from common import helpers
59 from common.exceptions import GajimGeneralException
60 from common import i18n
61 from common import pep
63 from message_window import MessageWindowMgr
65 from common import dbus_support
66 if dbus_support.supported:
67 import dbus
69 from common.xmpp.protocol import NS_FILE
70 from common.pep import MOODS, ACTIVITIES
72 #(icon, name, type, jid, account, editable, second pixbuf)
74 C_IMG, # image to show state (online, new message etc)
75 C_NAME, # cellrenderer text that holds contact nickame
76 C_TYPE, # account, group or contact?
77 C_JID, # the jid of the row
78 C_ACCOUNT, # cellrenderer text that holds account name
79 C_MOOD_PIXBUF,
80 C_ACTIVITY_PIXBUF,
81 C_TUNE_PIXBUF,
82 C_AVATAR_PIXBUF, # avatar_pixbuf
83 C_PADLOCK_PIXBUF, # use for account row only
84 ) = range(10)
86 class RosterWindow:
87 '''Class for main window of the GTK+ interface'''
89 def _get_account_iter(self, name, model=None):
90 '''
91 Return the gtk.TreeIter of the given account or None
92 if not found.
94 Keyword arguments:
95 name -- the account name
96 model -- the data model (default TreeFilterModel)
97 '''
98 if not model:
99 model = self.modelfilter
100 if model is None:
101 return
102 account_iter = model.get_iter_root()
103 if self.regroup:
104 return account_iter
105 while account_iter:
106 account_name = model[account_iter][C_ACCOUNT]
107 if account_name and name == account_name.decode('utf-8'):
108 break
109 account_iter = model.iter_next(account_iter)
110 return account_iter
113 def _get_group_iter(self, name, account, account_iter=None, model=None):
115 Return the gtk.TreeIter of the given group or None if not found.
117 Keyword arguments:
118 name -- the group name
119 account -- the account name
120 account_iter -- the iter of the account the model (default None)
121 model -- the data model (default TreeFilterModel)
124 if not model:
125 model = self.modelfilter
126 if not account_iter:
127 account_iter = self._get_account_iter(account, model)
128 group_iter = model.iter_children(account_iter)
129 # C_NAME column contacts the pango escaped group name
130 while group_iter:
131 group_name = model[group_iter][C_JID].decode('utf-8')
132 if name == group_name:
133 break
134 group_iter = model.iter_next(group_iter)
135 return group_iter
138 def _get_self_contact_iter(self, account, model=None):
139 ''' Return the gtk.TreeIter of SelfContact or None if not found.
141 Keyword arguments:
142 account -- the account of SelfContact
143 model -- the data model (default TreeFilterModel)
147 if not model:
148 model = self.modelfilter
149 iterAcct = self._get_account_iter(account, model)
150 iterC = model.iter_children(iterAcct)
152 # There might be several SelfContacts in merged account view
153 while iterC:
154 if model[iterC][C_TYPE] != 'self_contact':
155 break
156 iter_account = model[iterC][C_ACCOUNT]
157 if account == iter_account.decode('utf-8'):
158 return iterC
159 iterC = model.iter_next(iterC)
160 return None
163 def _get_contact_iter(self, jid, account, contact=None, model=None):
164 ''' Return a list of gtk.TreeIter of the given contact.
166 Keyword arguments:
167 jid -- the jid without resource
168 account -- the account
169 contact -- the contact (default None)
170 model -- the data model (default TreeFilterModel)
173 if not model:
174 model = self.modelfilter
175 # when closing Gajim model can be none (async pbs?)
176 if model is None:
177 return []
179 if jid == gajim.get_jid_from_account(account):
180 contact_iter = self._get_self_contact_iter(account, model)
181 if contact_iter:
182 return [contact_iter]
183 else:
184 return []
186 if not contact:
187 contact = gajim.contacts.get_first_contact_from_jid(account, jid)
188 if not contact:
189 # We don't know this contact
190 return []
192 acct = self._get_account_iter(account, model)
193 found = [] # the contact iters. One per group
194 for group in contact.get_shown_groups():
195 group_iter = self._get_group_iter(group, account, acct, model)
196 contact_iter = model.iter_children(group_iter)
198 while contact_iter:
199 # Loop over all contacts in this group
200 iter_jid = model[contact_iter][C_JID]
201 if iter_jid and jid == iter_jid.decode('utf-8') and \
202 account == model[contact_iter][C_ACCOUNT].decode('utf-8'):
203 # only one iter per group
204 found.append(contact_iter)
205 contact_iter = None
206 elif model.iter_has_child(contact_iter):
207 # it's a big brother and has children
208 contact_iter = model.iter_children(contact_iter)
209 else:
210 # try to find next contact:
211 # other contact in this group or
212 # brother contact
213 next_contact_iter = model.iter_next(contact_iter)
214 if next_contact_iter:
215 contact_iter = next_contact_iter
216 else:
217 # It's the last one.
218 # Go up if we are big brother
219 parent_iter = model.iter_parent(contact_iter)
220 if parent_iter and model[parent_iter][C_TYPE] == 'contact':
221 contact_iter = model.iter_next(parent_iter)
222 else:
223 # we tested all
224 # contacts in this group
225 contact_iter = None
226 return found
229 def _iter_is_separator(self, model, titer):
230 ''' Return True if the given iter is a separator.
232 Keyword arguments:
233 model -- the data model
234 iter -- the gtk.TreeIter to test
236 if model[titer][0] == 'SEPARATOR':
237 return True
238 return False
241 def _iter_contact_rows(self, model=None):
242 '''Iterate over all contact rows in given model.
244 Keyword argument
245 model -- the data model (default TreeFilterModel)
247 if not model:
248 model = self.modelfilter
249 account_iter = model.get_iter_root()
250 while account_iter:
251 group_iter = model.iter_children(account_iter)
252 while group_iter:
253 contact_iter = model.iter_children(group_iter)
254 while contact_iter:
255 yield model[contact_iter]
256 contact_iter = model.iter_next(
257 contact_iter)
258 group_iter = model.iter_next(group_iter)
259 account_iter = model.iter_next(account_iter)
262 #############################################################################
263 ### Methods for adding and removing roster window items
264 #############################################################################
266 def add_account(self, account):
268 Add account to roster and draw it. Do nothing if it is
269 already in.
271 if self._get_account_iter(account):
272 # Will happen on reconnect or for merged accounts
273 return
275 if self.regroup:
276 # Merged accounts view
277 show = helpers.get_global_show()
278 self.model.append(None, [
279 gajim.interface.jabber_state_images['16'][show],
280 _('Merged accounts'), 'account', '', 'all',
281 None, None, None, None, None])
282 else:
283 show = gajim.SHOW_LIST[gajim.connections[account].connected]
284 our_jid = gajim.get_jid_from_account(account)
286 tls_pixbuf = None
287 if gajim.account_is_securely_connected(account):
288 # the only way to create a pixbuf from stock
289 tls_pixbuf = self.window.render_icon(
290 gtk.STOCK_DIALOG_AUTHENTICATION,
291 gtk.ICON_SIZE_MENU)
293 self.model.append(None, [
294 gajim.interface.jabber_state_images['16'][show],
295 gobject.markup_escape_text(account), 'account',
296 our_jid, account, None, None, None, None,
297 tls_pixbuf])
299 self.draw_account(account)
302 def add_account_contacts(self, account):
303 '''Add all contacts and groups of the given account to roster,
304 draw them and account.
306 self.starting = True
307 jids = gajim.contacts.get_jid_list(account)
309 self.tree.freeze_child_notify()
310 for jid in jids:
311 self.add_contact(jid, account)
312 self.tree.thaw_child_notify()
314 # Do not freeze the GUI when drawing the contacts
315 if jids:
316 # Overhead is big, only invoke when needed
317 self._idle_draw_jids_of_account(jids, account)
319 # Draw all known groups
320 for group in gajim.groups[account]:
321 self.draw_group(group, account)
322 self.draw_account(account)
323 self.starting = False
326 def _add_entity(self, contact, account, groups=None,
327 big_brother_contact=None, big_brother_account=None):
328 '''Add the given contact to roster data model.
330 Contact is added regardless if he is already in roster or not.
331 Return list of newly added iters.
333 Keyword arguments:
334 contact -- the contact to add
335 account -- the contacts account
336 groups -- list of groups to add the contact to.
337 (default groups in contact.get_shown_groups()).
338 Parameter ignored when big_brother_contact is specified.
339 big_brother_contact -- if specified contact is added as child
340 big_brother_contact. (default None)
342 added_iters = []
343 if big_brother_contact:
344 # Add contact under big brother
346 parent_iters = self._get_contact_iter(
347 big_brother_contact.jid, big_brother_account,
348 big_brother_contact, self.model)
349 assert len(parent_iters) > 0, 'Big brother is not yet in roster!'
351 # Do not confuse get_contact_iter: Sync groups of family members
352 contact.groups = big_brother_contact.get_shown_groups()[:]
354 for child_iter in parent_iters:
355 it = self.model.append(child_iter, (None, contact.get_shown_name(),
356 'contact', contact.jid, account, None, None, None, None, None))
357 added_iters.append(it)
358 else:
359 # We are a normal contact. Add us to our groups.
360 if not groups:
361 groups = contact.get_shown_groups()
362 for group in groups:
363 child_iterG = self._get_group_iter(group, account,
364 model = self.model)
365 if not child_iterG:
366 # Group is not yet in roster, add it!
367 child_iterA = self._get_account_iter(account, self.model)
368 child_iterG = self.model.append(child_iterA,
369 [gajim.interface.jabber_state_images['16']['closed'],
370 gobject.markup_escape_text(group),
371 'group', group, account, None, None, None, None, None])
372 self.draw_group(group, account)
374 if contact.is_transport():
375 typestr = 'agent'
376 elif contact.is_groupchat():
377 typestr = 'groupchat'
378 else:
379 typestr = 'contact'
381 # we add some values here. see draw_contact
382 # for more
383 i_ = self.model.append(child_iterG, (None,
384 contact.get_shown_name(), typestr,
385 contact.jid, account, None, None, None,
386 None, None))
387 added_iters.append(i_)
389 # Restore the group expand state
390 if account + group in self.collapsed_rows:
391 is_expanded = False
392 else:
393 is_expanded = True
394 if group not in gajim.groups[account]:
395 gajim.groups[account][group] = {'expand': is_expanded}
397 assert len(added_iters), '%s has not been added to roster!' % contact.jid
398 return added_iters
400 def _remove_entity(self, contact, account, groups=None):
401 '''Remove the given contact from roster data model.
403 Empty groups after contact removal are removed too.
404 Return False if contact still has children and deletion was
405 not performed.
406 Return True on success.
408 Keyword arguments:
409 contact -- the contact to add
410 account -- the contacts account
411 groups -- list of groups to remove the contact from.
413 iters = self._get_contact_iter(contact.jid, account, contact, self.model)
414 assert iters, '%s shall be removed but is not in roster' % contact.jid
416 parent_iter = self.model.iter_parent(iters[0])
417 parent_type = self.model[parent_iter][C_TYPE]
419 if groups:
420 # Only remove from specified groups
421 all_iters = iters[:]
422 group_iters = [self._get_group_iter(group, account)
423 for group in groups]
424 iters = [titer for titer in all_iters
425 if self.model.iter_parent(titer) in group_iters]
427 iter_children = self.model.iter_children(iters[0])
429 if iter_children:
430 # We have children. We cannot be removed!
431 return False
432 else:
433 # Remove us and empty groups from the model
434 for i in iters:
435 assert self.model[i][C_JID] == contact.jid and \
436 self.model[i][C_ACCOUNT] == account, \
437 "Invalidated iters of %s" % contact.jid
439 parent_i = self.model.iter_parent(i)
441 if parent_type == 'group' and \
442 self.model.iter_n_children(parent_i) == 1:
443 group = self.model[parent_i][C_JID].decode('utf-8')
444 if group in gajim.groups[account]:
445 del gajim.groups[account][group]
446 self.model.remove(parent_i)
447 else:
448 self.model.remove(i)
449 return True
451 def _add_metacontact_family(self, family, account):
453 Add the give Metacontact family to roster data model.
455 Add Big Brother to his groups and all others under him.
456 Return list of all added (contact, account) tuples with
457 Big Brother as first element.
459 Keyword arguments:
460 family -- the family, see Contacts.get_metacontacts_family()
463 nearby_family, big_brother_jid, big_brother_account = \
464 self._get_nearby_family_and_big_brother(family, account)
465 big_brother_contact = gajim.contacts.get_first_contact_from_jid(
466 big_brother_account, big_brother_jid)
468 assert len(self._get_contact_iter(big_brother_jid,
469 big_brother_account, big_brother_contact, self.model)) == 0, \
470 'Big brother %s already in roster\n Family: %s' \
471 % (big_brother_jid, family)
472 self._add_entity(big_brother_contact, big_brother_account)
474 brothers = []
475 # Filter family members
476 for data in nearby_family:
477 _account = data['account']
478 _jid = data['jid']
479 _contact = gajim.contacts.get_first_contact_from_jid(
480 _account, _jid)
482 if not _contact or _contact == big_brother_contact:
483 # Corresponding account is not connected
484 # or brother already added
485 continue
487 assert len(self._get_contact_iter(_jid, _account,
488 _contact, self.model)) == 0, \
489 "%s already in roster.\n Family: %s" % (_jid, nearby_family)
490 self._add_entity(_contact, _account,
491 big_brother_contact = big_brother_contact,
492 big_brother_account = big_brother_account)
493 brothers.append((_contact, _account))
495 brothers.insert(0, (big_brother_contact, big_brother_account))
496 return brothers
498 def _remove_metacontact_family(self, family, account):
500 Remove the given Metacontact family from roster data model.
502 See Contacts.get_metacontacts_family() and
503 RosterWindow._remove_entity()
505 nearby_family = self._get_nearby_family_and_big_brother(
506 family, account)[0]
508 # Family might has changed (actual big brother not on top).
509 # Remove childs first then big brother
510 family_in_roster = False
511 for data in nearby_family:
512 _account = data['account']
513 _jid = data['jid']
514 _contact = gajim.contacts.get_first_contact_from_jid(_account, _jid)
516 iters = self._get_contact_iter(_jid, _account, _contact, self.model)
517 if not iters or not _contact:
518 # Family might not be up to date.
519 # Only try to remove what is actually in the roster
520 continue
521 assert iters, '%s shall be removed but is not in roster \
522 \n Family: %s' % (_jid, family)
524 family_in_roster = True
526 parent_iter = self.model.iter_parent(iters[0])
527 parent_type = self.model[parent_iter][C_TYPE]
529 if parent_type != 'contact':
530 # The contact on top
531 old_big_account = _account
532 old_big_contact = _contact
533 old_big_jid = _jid
534 continue
536 ok = self._remove_entity(_contact, _account)
537 assert ok, '%s was not removed' % _jid
538 assert len(self._get_contact_iter(_jid, _account, _contact,
539 self.model)) == 0, '%s is removed but still in roster' % _jid
541 if not family_in_roster:
542 return False
544 assert old_big_jid, 'No Big Brother in nearby family % (Family: %)' % \
545 (nearby_family, family)
546 iters = self._get_contact_iter(old_big_jid, old_big_account,
547 old_big_contact, self.model)
548 assert len(iters) > 0, 'Old Big Brother %s is not in roster anymore' % \
549 old_big_jid
550 assert not self.model.iter_children(iters[0]),\
551 'Old Big Brother %s still has children' % old_big_jid
553 ok = self._remove_entity(old_big_contact, old_big_account)
554 assert ok, "Old Big Brother %s not removed" % old_big_jid
555 assert len(self._get_contact_iter(old_big_jid, old_big_account,
556 old_big_contact, self.model)) == 0,\
557 'Old Big Brother %s is removed but still in roster' % old_big_jid
559 return True
562 def _recalibrate_metacontact_family(self, family, account):
563 '''Regroup metacontact family if necessary.'''
565 brothers = []
566 nearby_family, big_brother_jid, big_brother_account = \
567 self._get_nearby_family_and_big_brother(family, account)
568 big_brother_contact = gajim.contacts.get_contact(big_brother_account,
569 big_brother_jid)
570 child_iters = self._get_contact_iter(big_brother_jid, big_brother_account,
571 model=self.model)
572 if child_iters:
573 parent_iter = self.model.iter_parent(child_iters[0])
574 parent_type = self.model[parent_iter][C_TYPE]
576 # Check if the current BigBrother has even been before.
577 if parent_type == 'contact':
578 for data in nearby_family:
579 # recalibrate after remove to keep highlight
580 if data['jid'] in gajim.to_be_removed[data['account']]:
581 return
583 self._remove_metacontact_family(family, account)
584 brothers = self._add_metacontact_family(family, account)
586 for c, acc in brothers:
587 self.draw_completely(c.jid, acc)
589 # Check is small brothers are under the big brother
590 for child in nearby_family:
591 _jid = child['jid']
592 _account = child['account']
593 if _account == big_brother_account and _jid == big_brother_jid:
594 continue
595 child_iters = self._get_contact_iter(_jid, _account, model=self.model)
596 if not child_iters:
597 continue
598 parent_iter = self.model.iter_parent(child_iters[0])
599 parent_type = self.model[parent_iter][C_TYPE]
600 if parent_type != 'contact':
601 _contact = gajim.contacts.get_contact(_account, _jid)
602 self._remove_entity(_contact, _account)
603 self._add_entity(_contact, _account, groups=None,
604 big_brother_contact=big_brother_contact,
605 big_brother_account=big_brother_account)
607 def _get_nearby_family_and_big_brother(self, family, account):
608 '''Return the nearby family and its Big Brother
610 Nearby family is the part of the family that is grouped with the metacontact.
611 A metacontact may be over different accounts. If regroup is s False the
612 given family is split account wise.
614 (nearby_family, big_brother_jid, big_brother_account)
616 if self.regroup:
617 # group all together
618 nearby_family = family
619 else:
620 # we want one nearby_family per account
621 nearby_family = [data for data in family
622 if account == data['account']]
624 big_brother_data = gajim.contacts.get_metacontacts_big_brother(
625 nearby_family)
626 big_brother_jid = big_brother_data['jid']
627 big_brother_account = big_brother_data['account']
629 return (nearby_family, big_brother_jid, big_brother_account)
632 def _add_self_contact(self, account):
633 '''Add account's SelfContact to roster and draw it and the account.
635 Return the SelfContact contact instance
637 jid = gajim.get_jid_from_account(account)
638 contact = gajim.contacts.get_first_contact_from_jid(account, jid)
640 assert len(self._get_contact_iter(jid, account, contact, self.model)) == \
641 0, 'Self contact %s already in roster' % jid
643 child_iterA = self._get_account_iter(account, self.model)
644 self.model.append(child_iterA, (None, gajim.nicks[account],
645 'self_contact', jid, account, None, None, None, None,
646 None))
648 self.draw_completely(jid, account)
649 self.draw_account(account)
651 return contact
654 def redraw_metacontacts(self, account):
655 for tag in gajim.contacts.get_metacontacts_tags(account):
656 family = gajim.contacts.get_metacontacts_family_from_tag(account, tag)
657 self._recalibrate_metacontact_family(family, account)
659 def add_contact(self, jid, account):
660 '''Add contact to roster and draw him.
662 Add contact to all its group and redraw the groups, the contact and the
663 account. If it's a Metacontact, add and draw the whole family.
664 Do nothing if the contact is already in roster.
666 Return the added contact instance. If it is a Metacontact return
667 Big Brother.
669 Keyword arguments:
670 jid -- the contact's jid or SelfJid to add SelfContact
671 account -- the corresponding account.
674 contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
675 if len(self._get_contact_iter(jid, account, contact, self.model)):
676 # If contact already in roster, do nothing
677 return
679 if jid == gajim.get_jid_from_account(account):
680 show_self_contact = gajim.config.get('show_self_contact')
681 if show_self_contact == 'never':
682 return
683 if (contact.resource != gajim.connections[account].server_resource and\
684 show_self_contact == 'when_other_resource') or show_self_contact == \
685 'always':
686 return self._add_self_contact(account)
687 return
689 is_observer = contact.is_observer()
690 if is_observer:
691 # if he has a tag, remove it
692 tag = gajim.contacts.get_metacontacts_tag(account, jid)
693 if tag:
694 gajim.contacts.remove_metacontact(account, jid)
696 # Add contact to roster
697 family = gajim.contacts.get_metacontacts_family(account, jid)
698 contacts = []
699 if family:
700 # We have a family. So we are a metacontact.
701 # Add all family members that we shall be grouped with
702 if self.regroup:
703 # remove existing family members to regroup them
704 self._remove_metacontact_family(family, account)
705 contacts = self._add_metacontact_family(family, account)
706 else:
707 # We are a normal contact
708 contacts = [(contact, account),]
709 self._add_entity(contact, account)
711 # Draw the contact and its groups contact
712 if not self.starting:
713 for c, acc in contacts:
714 self.draw_completely(c.jid, acc)
715 for group in contact.get_shown_groups():
716 self.draw_group(group, account)
717 self._adjust_group_expand_collapse_state(group, account)
718 self.draw_account(account)
720 return contacts[0][0] # it's contact/big brother with highest priority
722 def remove_contact(self, jid, account, force=False, backend=False):
723 '''Remove contact from roster.
725 Remove contact from all its group. Remove empty groups or redraw
726 otherwise.
727 Draw the account.
728 If it's a Metacontact, remove the whole family.
729 Do nothing if the contact is not in roster.
731 Keyword arguments:
732 jid -- the contact's jid or SelfJid to remove SelfContact
733 account -- the corresponding account.
734 force -- remove contact even it has pending evens (Default False)
735 backend -- also remove contact instance (Default False)
738 contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
739 if not contact:
740 return
742 if not force and (self.contact_has_pending_roster_events(contact,
743 account) or gajim.interface.msg_win_mgr.get_control(jid, account)):
744 # Contact has pending events or window
745 #TODO: or single message windows? Bur they are not listed for the
746 # moment
747 key = (jid, account)
748 if not key in self.contacts_to_be_removed:
749 self.contacts_to_be_removed[key] = {'backend': backend}
750 # if more pending event, don't remove from roster
751 if self.contact_has_pending_roster_events(contact, account):
752 return False
754 iters = self._get_contact_iter(jid, account, contact, self.model)
755 if iters:
756 # no more pending events
757 # Remove contact from roster directly
758 family = gajim.contacts.get_metacontacts_family(account, jid)
759 if family:
760 # We have a family. So we are a metacontact.
761 self._remove_metacontact_family(family, account)
762 else:
763 self._remove_entity(contact, account)
765 if backend and (not gajim.interface.msg_win_mgr.get_control(jid, account)\
766 or force):
767 # If a window is still opened: don't remove contact instance
768 # Remove contact before redrawing, otherwise the old
769 # numbers will still be show
770 gajim.contacts.remove_jid(account, jid, remove_meta=True)
771 if iters:
772 rest_of_family = [data for data in family
773 if account != data['account'] or jid != data['jid']]
774 if rest_of_family:
775 # reshow the rest of the family
776 brothers = self._add_metacontact_family(rest_of_family, account)
777 for c, acc in brothers:
778 self.draw_completely(c.jid, acc)
780 if iters:
781 # Draw all groups of the contact
782 for group in contact.get_shown_groups():
783 self.draw_group(group, account)
784 self.draw_account(account)
786 return True
788 def rename_self_contact(self, old_jid, new_jid, account):
789 '''Rename the self_contact jid
791 Keyword arguments:
792 old_jid -- our old jid
793 new_jid -- our new jid
794 account -- the corresponding account.
796 gajim.contacts.change_contact_jid(old_jid, new_jid, account)
797 self_iter = self._get_self_contact_iter(account, model=self.model)
798 if not self_iter:
799 return
800 self.model[self_iter][C_JID] = new_jid
801 self.draw_contact(new_jid, account)
803 def add_groupchat(self, jid, account, status=''):
804 '''Add groupchat to roster and draw it.
805 Return the added contact instance.
807 contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
808 # Do not show gc if we are disconnected and minimize it
809 if gajim.account_is_connected(account):
810 show = 'online'
811 else:
812 show = 'offline'
813 status = ''
815 if contact is None:
816 gc_control = gajim.interface.msg_win_mgr.get_gc_control(jid, account)
817 if gc_control:
818 # there is a window that we can minimize
819 gajim.interface.minimized_controls[account][jid] = gc_control
820 name = gc_control.name
821 elif jid in gajim.interface.minimized_controls[account]:
822 name = gajim.interface.minimized_controls[account][jid].name
823 else:
824 name = jid.split('@')[0]
825 # New groupchat
826 contact = gajim.contacts.create_contact(jid=jid, name=name,
827 groups=[_('Groupchats')], show=show, status=status, sub='none')
828 gajim.contacts.add_contact(account, contact)
829 self.add_contact(jid, account)
830 else:
831 if jid not in gajim.interface.minimized_controls[account]:
832 # there is a window that we can minimize
833 gc_control = gajim.interface.msg_win_mgr.get_gc_control(jid,
834 account)
835 gajim.interface.minimized_controls[account][jid] = gc_control
836 contact.show = show
837 contact.status = status
838 self.adjust_and_draw_contact_context(jid, account)
840 return contact
843 def remove_groupchat(self, jid, account):
844 '''Remove groupchat from roster and redraw account and group.'''
845 contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
846 if contact.is_groupchat():
847 if jid in gajim.interface.minimized_controls[account]:
848 del gajim.interface.minimized_controls[account][jid]
849 self.remove_contact(jid, account, force=True, backend=True)
850 return True
851 else:
852 return False
855 # FIXME: This function is yet unused! Port to new API
856 def add_transport(self, jid, account):
857 '''Add transport to roster and draw it.
858 Return the added contact instance.'''
859 contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
860 if contact is None:
861 contact = gajim.contacts.create_contact(jid=jid, name=jid,
862 groups=[_('Transports')], show='offline', status='offline',
863 sub='from')
864 gajim.contacts.add_contact(account, contact)
865 self.add_contact(jid, account)
866 return contact
868 def remove_transport(self, jid, account):
869 '''Remove transport from roster and redraw account and group.'''
870 self.remove_contact(jid, account, force=True, backend=True)
871 return True
873 def rename_group(self, old_name, new_name, account):
875 rename a roster group
877 if old_name == new_name:
878 return
880 # Groups may not change name from or to a special groups
881 for g in helpers.special_groups:
882 if g in (new_name, old_name):
883 return
885 # update all contacts in the given group
886 if self.regroup:
887 accounts = gajim.connections.keys()
888 else:
889 accounts = [account,]
891 for acc in accounts:
892 changed_contacts = []
893 for jid in gajim.contacts.get_jid_list(acc):
894 contact = gajim.contacts.get_first_contact_from_jid(acc, jid)
895 if old_name not in contact.groups:
896 continue
898 self.remove_contact(jid, acc, force=True)
900 contact.groups.remove(old_name)
901 if new_name not in contact.groups:
902 contact.groups.append(new_name)
904 changed_contacts.append({'jid':jid, 'name':contact.name,
905 'groups':contact.groups})
907 gajim.connections[acc].update_contacts(changed_contacts)
909 for c in changed_contacts:
910 self.add_contact(c['jid'], acc)
912 self._adjust_group_expand_collapse_state(new_name, acc)
914 self.draw_group(old_name, acc)
915 self.draw_group(new_name, acc)
918 def add_contact_to_groups(self, jid, account, groups, update=True):
919 '''Add contact to given groups and redraw them.
921 Contact on server is updated too. When the contact has a family,
922 the action will be performed for all members.
924 Keyword Arguments:
925 jid -- the jid
926 account -- the corresponding account
927 groups -- list of Groups to add the contact to.
928 update -- update contact on the server
931 self.remove_contact(jid, account, force=True)
932 for contact in gajim.contacts.get_contacts(account, jid):
933 for group in groups:
934 if group not in contact.groups:
935 # we might be dropped from meta to group
936 contact.groups.append(group)
937 if update:
938 gajim.connections[account].update_contact(jid, contact.name,
939 contact.groups)
941 self.add_contact(jid, account)
943 for group in groups:
944 self._adjust_group_expand_collapse_state(group, account)
946 def remove_contact_from_groups(self, jid, account, groups, update=True):
947 '''Remove contact from given groups and redraw them.
949 Contact on server is updated too. When the contact has a family,
950 the action will be performed for all members.
952 Keyword Arguments:
953 jid -- the jid
954 account -- the corresponding account
955 groups -- list of Groups to remove the contact from
956 update -- update contact on the server
959 self.remove_contact(jid, account, force=True)
960 for contact in gajim.contacts.get_contacts(account, jid):
961 for group in groups:
962 if group in contact.groups:
963 # Needed when we remove from "General" or "Observers"
964 contact.groups.remove(group)
965 if update:
966 gajim.connections[account].update_contact(jid, contact.name,
967 contact.groups)
968 self.add_contact(jid, account)
970 # Also redraw old groups
971 for group in groups:
972 self.draw_group(group, account)
974 # FIXME: maybe move to gajim.py
975 def remove_newly_added(self, jid, account):
976 if jid in gajim.newly_added[account]:
977 gajim.newly_added[account].remove(jid)
978 self.draw_contact(jid, account)
980 # FIXME: maybe move to gajim.py
981 def remove_to_be_removed(self, jid, account):
982 if account not in gajim.interface.instances:
983 # Account has been deleted during the timeout that called us
984 return
985 if jid in gajim.newly_added[account]:
986 return
987 if jid in gajim.to_be_removed[account]:
988 gajim.to_be_removed[account].remove(jid)
989 family = gajim.contacts.get_metacontacts_family(account, jid)
990 if family:
991 # Peform delayed recalibration
992 self._recalibrate_metacontact_family(family, account)
993 self.draw_contact(jid, account)
995 #FIXME: integrate into add_contact()
996 def add_to_not_in_the_roster(self, account, jid, nick='', resource=''):
997 keyID = ''
998 attached_keys = gajim.config.get_per('accounts', account,
999 'attached_gpg_keys').split()
1000 if jid in attached_keys:
1001 keyID = attached_keys[attached_keys.index(jid) + 1]
1002 contact = gajim.contacts.create_contact(jid=jid, name=nick,
1003 groups=[_('Not in Roster')], show='not in roster', status='',
1004 sub='none', resource=resource, keyID=keyID)
1005 gajim.contacts.add_contact(account, contact)
1006 self.add_contact(contact.jid, account)
1007 return contact
1010 ################################################################################
1011 ### Methods for adding and removing roster window items
1012 ################################################################################
1014 def draw_account(self, account):
1015 child_iter = self._get_account_iter(account, self.model)
1016 if not child_iter:
1017 assert False, 'Account iter of %s could not be found.' % account
1018 return
1020 num_of_accounts = gajim.get_number_of_connected_accounts()
1021 num_of_secured = gajim.get_number_of_securely_connected_accounts()
1023 if gajim.account_is_securely_connected(account) and not self.regroup or \
1024 self.regroup and num_of_secured and num_of_secured == num_of_accounts:
1025 tls_pixbuf = self.window.render_icon(gtk.STOCK_DIALOG_AUTHENTICATION,
1026 gtk.ICON_SIZE_MENU) # the only way to create a pixbuf from stock
1027 self.model[child_iter][C_PADLOCK_PIXBUF] = tls_pixbuf
1028 else:
1029 self.model[child_iter][C_PADLOCK_PIXBUF] = None
1031 if self.regroup:
1032 account_name = _('Merged accounts')
1033 accounts = []
1034 else:
1035 account_name = account
1036 accounts = [account]
1038 if account in self.collapsed_rows and \
1039 self.model.iter_has_child(child_iter):
1040 account_name = '[%s]' % account_name
1042 if (gajim.account_is_connected(account) or (self.regroup and \
1043 gajim.get_number_of_connected_accounts())) and gajim.config.get(
1044 'show_contacts_number'):
1045 nbr_on, nbr_total = gajim.contacts.get_nb_online_total_contacts(
1046 accounts = accounts)
1047 account_name += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total))
1049 self.model[child_iter][C_NAME] = account_name
1051 if gajim.config.get('show_mood_in_roster') \
1052 and 'mood' in gajim.connections[account].mood \
1053 and gajim.connections[account].mood['mood'].strip() in MOODS:
1055 self.model[child_iter][C_MOOD_PIXBUF] = gtkgui_helpers.load_mood_icon(
1056 gajim.connections[account].mood['mood'].strip()).get_pixbuf()
1058 elif gajim.config.get('show_mood_in_roster') \
1059 and 'mood' in gajim.connections[account].mood:
1060 self.model[child_iter][C_MOOD_PIXBUF] = \
1061 gtkgui_helpers.load_mood_icon('unknown'). \
1062 get_pixbuf()
1063 else:
1064 self.model[child_iter][C_MOOD_PIXBUF] = None
1066 if gajim.config.get('show_activity_in_roster') \
1067 and 'activity' in gajim.connections[account].activity \
1068 and gajim.connections[account].activity['activity'].strip() \
1069 in ACTIVITIES:
1070 if 'subactivity' in gajim.connections[account].activity \
1071 and gajim.connections[account].activity['subactivity'].strip() \
1072 in ACTIVITIES[gajim.connections[account].activity['activity'].strip()]:
1073 self.model[child_iter][C_ACTIVITY_PIXBUF] = \
1074 gtkgui_helpers.load_activity_icon(
1075 gajim.connections[account].activity['activity'].strip(),
1076 gajim.connections[account].activity['subactivity'].strip()). \
1077 get_pixbuf()
1078 else:
1079 self.model[child_iter][C_ACTIVITY_PIXBUF] = \
1080 gtkgui_helpers.load_activity_icon(
1081 gajim.connections[account].activity['activity'].strip()). \
1082 get_pixbuf()
1083 elif gajim.config.get('show_activity_in_roster') \
1084 and 'activity' in gajim.connections[account].activity:
1085 self.model[child_iter][C_ACTIVITY_PIXBUF] = \
1086 gtkgui_helpers.load_activity_icon('unknown'). \
1087 get_pixbuf()
1088 else:
1089 self.model[child_iter][C_ACTIVITY_PIXBUF] = None
1091 if gajim.config.get('show_tunes_in_roster') \
1092 and ('artist' in gajim.connections[account].tune \
1093 or 'title' in gajim.connections[account].tune):
1094 path = os.path.join(gajim.DATA_DIR, 'emoticons', 'static', 'music.png')
1095 self.model[child_iter][C_TUNE_PIXBUF] = \
1096 gtk.gdk.pixbuf_new_from_file(path)
1097 else:
1098 self.model[child_iter][C_TUNE_PIXBUF] = None
1100 return False
1102 def draw_group(self, group, account):
1103 child_iter = self._get_group_iter(group, account, model=self.model)
1104 if not child_iter:
1105 # Eg. We redraw groups after we removed a entitiy
1106 # and its empty groups
1107 return
1108 if self.regroup:
1109 accounts = []
1110 else:
1111 accounts = [account]
1112 text = gobject.markup_escape_text(group)
1113 if helpers.group_is_blocked(account, group):
1114 text = '<span strikethrough="true">%s</span>' % text
1115 if gajim.config.get('show_contacts_number'):
1116 nbr_on, nbr_total = gajim.contacts.get_nb_online_total_contacts(
1117 accounts = accounts, groups = [group])
1118 text += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total))
1120 self.model[child_iter][C_NAME] = text
1121 return False
1123 def draw_parent_contact(self, jid, account):
1124 child_iters = self._get_contact_iter(jid, account, model=self.model)
1125 if not child_iters:
1126 return False
1127 parent_iter = self.model.iter_parent(child_iters[0])
1128 if self.model[parent_iter][C_TYPE] != 'contact':
1129 # parent is not a contact
1130 return
1131 parent_jid = self.model[parent_iter][C_JID].decode('utf-8')
1132 parent_account = self.model[parent_iter][C_ACCOUNT].decode('utf-8')
1133 self.draw_contact(parent_jid, parent_account)
1134 return False
1136 def draw_contact(self, jid, account, selected=False, focus=False):
1137 '''draw the correct state image, name BUT not avatar'''
1138 # focus is about if the roster window has toplevel-focus or not
1139 # FIXME: We really need a custom cell_renderer
1141 contact_instances = gajim.contacts.get_contacts(account, jid)
1142 contact = gajim.contacts.get_highest_prio_contact_from_contacts(
1143 contact_instances)
1145 child_iters = self._get_contact_iter(jid, account, contact, self.model)
1146 if not child_iters:
1147 return False
1149 name = gobject.markup_escape_text(contact.get_shown_name())
1151 # gets number of unread gc marked messages
1152 if jid in gajim.interface.minimized_controls[account] and \
1153 gajim.interface.minimized_controls[account][jid]:
1154 nb_unread = len(gajim.events.get_events(account, jid,
1155 ['printed_marked_gc_msg']))
1156 nb_unread += gajim.interface.minimized_controls \
1157 [account][jid].get_nb_unread_pm()
1159 if nb_unread == 1:
1160 name = '%s *' % name
1161 elif nb_unread > 1:
1162 name = '%s [%s]' % (name, str(nb_unread))
1164 # Strike name if blocked
1165 strike = False
1166 if helpers.jid_is_blocked(account, jid):
1167 strike = True
1168 else:
1169 for group in contact.get_shown_groups():
1170 if helpers.group_is_blocked(account, group):
1171 strike = True
1172 break
1173 if strike:
1174 name = '<span strikethrough="true">%s</span>' % name
1176 # Show resource counter
1177 nb_connected_contact = 0
1178 for c in contact_instances:
1179 if c.show not in ('error', 'offline'):
1180 nb_connected_contact += 1
1181 if nb_connected_contact > 1:
1182 # switch back to default writing direction
1183 name += i18n.paragraph_direction_mark(unicode(name))
1184 name += u' (%d)' % nb_connected_contact
1186 # show (account_name) if there are 2 contact with same jid
1187 # in merged mode
1188 if self.regroup:
1189 add_acct = False
1190 # look through all contacts of all accounts
1191 for account_ in gajim.connections:
1192 # useless to add account name
1193 if account_ == account:
1194 continue
1195 for jid_ in gajim.contacts.get_jid_list(account_):
1196 contact_ = gajim.contacts.get_first_contact_from_jid(
1197 account_, jid_)
1198 if contact_.get_shown_name() == contact.get_shown_name() and \
1199 (jid_, account_) != (jid, account):
1200 add_acct = True
1201 break
1202 if add_acct:
1203 # No need to continue in other account
1204 # if we already found one
1205 break
1206 if add_acct:
1207 name += ' (' + account + ')'
1209 # add status msg, if not empty, under contact name in
1210 # the treeview
1211 if contact.status and gajim.config.get('show_status_msgs_in_roster'):
1212 status = contact.status.strip()
1213 if status != '':
1214 status = helpers.reduce_chars_newlines(status,
1215 max_lines = 1)
1216 # escape markup entities and make them small
1217 # italic and fg color color is calcuted to be
1218 # always readable
1219 color = gtkgui_helpers._get_fade_color(self.tree, selected, focus)
1220 colorstring = '#%04x%04x%04x' % (color.red, color.green, color.blue)
1221 name += '\n<span size="small" style="italic" ' \
1222 'foreground="%s">%s</span>' % (
1223 colorstring,
1224 gobject.markup_escape_text(status))
1226 icon_name = helpers.get_icon_name_to_show(contact, account)
1227 # look if another resource has awaiting events
1228 for c in contact_instances:
1229 c_icon_name = helpers.get_icon_name_to_show(c, account)
1230 if c_icon_name in ('event', 'muc_active', 'muc_inactive'):
1231 icon_name = c_icon_name
1232 break
1234 # Check for events of collapsed (hidden) brothers
1235 family = gajim.contacts.get_metacontacts_family(account, jid)
1236 is_big_brother = False
1237 have_visible_children = False
1238 if family:
1239 bb_jid, bb_account = \
1240 self._get_nearby_family_and_big_brother(family, account)[1:]
1241 is_big_brother = (jid, account) == (bb_jid, bb_account)
1242 iters = self._get_contact_iter(jid, account)
1243 have_visible_children = iters \
1244 and self.modelfilter.iter_has_child(iters[0])
1246 if have_visible_children:
1247 # We are the big brother and have a visible family
1248 for child_iter in child_iters:
1249 child_path = self.model.get_path(child_iter)
1250 path = self.modelfilter.convert_child_path_to_path(child_path)
1252 if not path:
1253 continue
1255 if not self.tree.row_expanded(path) and icon_name != 'event':
1256 iterC = self.model.iter_children(child_iter)
1257 while iterC:
1258 # a child has awaiting messages?
1259 jidC = self.model[iterC][C_JID].decode('utf-8')
1260 accountC = self.model[iterC][C_ACCOUNT].decode('utf-8')
1261 if len(gajim.events.get_events(accountC, jidC)):
1262 icon_name = 'event'
1263 break
1264 iterC = self.model.iter_next(iterC)
1266 if self.tree.row_expanded(path):
1267 state_images = self.get_appropriate_state_images(
1268 jid, size = 'opened',
1269 icon_name = icon_name)
1270 else:
1271 state_images = self.get_appropriate_state_images(
1272 jid, size = 'closed',
1273 icon_name = icon_name)
1275 # Expand/collapse icon might differ per iter
1276 # (group)
1277 img = state_images[icon_name]
1278 self.model[child_iter][C_IMG] = img
1279 self.model[child_iter][C_NAME] = name
1280 else:
1281 # A normal contact or little brother
1282 state_images = self.get_appropriate_state_images(jid,
1283 icon_name = icon_name)
1285 # All iters have the same icon (no expand/collapse)
1286 img = state_images[icon_name]
1287 for child_iter in child_iters:
1288 self.model[child_iter][C_IMG] = img
1289 self.model[child_iter][C_NAME] = name
1291 # We are a little brother
1292 if family and not is_big_brother and not self.starting:
1293 self.draw_parent_contact(jid, account)
1295 for group in contact.get_shown_groups():
1296 # We need to make sure that _visible_func is called for
1297 # our groups otherwise we might not be shown
1298 iterG = self._get_group_iter(group, account, model=self.model)
1299 if iterG:
1300 # it's not self contact
1301 self.model[iterG][C_JID] = self.model[iterG][C_JID]
1303 return False
1306 def draw_mood(self, jid, account):
1307 iters = self._get_contact_iter(jid, account, model=self.model)
1308 if not iters or not gajim.config.get('show_mood_in_roster'):
1309 return
1310 jid = self.model[iters[0]][C_JID]
1311 jid = jid.decode('utf-8')
1312 contact = gajim.contacts.get_contact(account, jid)
1313 if 'mood' in contact.mood and contact.mood['mood'].strip() in MOODS:
1314 pixbuf = gtkgui_helpers.load_mood_icon(
1315 contact.mood['mood'].strip()).get_pixbuf()
1316 elif 'mood' in contact.mood:
1317 pixbuf = gtkgui_helpers.load_mood_icon(
1318 'unknown').get_pixbuf()
1319 else:
1320 pixbuf = None
1321 for child_iter in iters:
1322 self.model[child_iter][C_MOOD_PIXBUF] = pixbuf
1323 return False
1326 def draw_activity(self, jid, account):
1327 iters = self._get_contact_iter(jid, account, model=self.model)
1328 if not iters or not gajim.config.get('show_activity_in_roster'):
1329 return
1330 jid = self.model[iters[0]][C_JID]
1331 jid = jid.decode('utf-8')
1332 contact = gajim.contacts.get_contact(account, jid)
1333 if 'activity' in contact.activity \
1334 and contact.activity['activity'].strip() in ACTIVITIES:
1335 if 'subactivity' in contact.activity \
1336 and contact.activity['subactivity'].strip() in \
1337 ACTIVITIES[contact.activity['activity'].strip()]:
1338 pixbuf = gtkgui_helpers.load_activity_icon(
1339 contact.activity['activity'].strip(),
1340 contact.activity['subactivity'].strip()).get_pixbuf()
1341 else:
1342 pixbuf = gtkgui_helpers.load_activity_icon(
1343 contact.activity['activity'].strip()).get_pixbuf()
1344 elif 'activity' in contact.activity:
1345 pixbuf = gtkgui_helpers.load_activity_icon(
1346 'unknown').get_pixbuf()
1347 else:
1348 pixbuf = None
1349 for child_iter in iters:
1350 self.model[child_iter][C_ACTIVITY_PIXBUF] = pixbuf
1351 return False
1354 def draw_tune(self, jid, account):
1355 iters = self._get_contact_iter(jid, account, model=self.model)
1356 if not iters or not gajim.config.get('show_tunes_in_roster'):
1357 return
1358 jid = self.model[iters[0]][C_JID]
1359 jid = jid.decode('utf-8')
1360 contact = gajim.contacts.get_contact(account, jid)
1361 if 'artist' in contact.tune or 'title' in contact.tune:
1362 path = os.path.join(gajim.DATA_DIR, 'emoticons', 'static', 'music.png')
1363 pixbuf = gtk.gdk.pixbuf_new_from_file(path)
1364 else:
1365 pixbuf = None
1366 for child_iter in iters:
1367 self.model[child_iter][C_TUNE_PIXBUF] = pixbuf
1368 return False
1371 def draw_avatar(self, jid, account):
1372 iters = self._get_contact_iter(jid, account, model=self.model)
1373 if not iters or not gajim.config.get('show_avatars_in_roster'):
1374 return
1375 jid = self.model[iters[0]][C_JID]
1376 jid = jid.decode('utf-8')
1377 pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid)
1378 if pixbuf is None or pixbuf == 'ask':
1379 scaled_pixbuf = None
1380 else:
1381 scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'roster')
1382 for child_iter in iters:
1383 self.model[child_iter][C_AVATAR_PIXBUF] = scaled_pixbuf
1384 return False
1386 def draw_completely(self, jid, account):
1387 self.draw_contact(jid, account)
1388 self.draw_mood(jid, account)
1389 self.draw_activity(jid, account)
1390 self.draw_tune(jid, account)
1391 self.draw_avatar(jid, account)
1393 def adjust_and_draw_contact_context(self, jid, account):
1394 '''Draw contact, account and groups of given jid
1395 Show contact if it has pending events
1397 contact = gajim.contacts.get_first_contact_from_jid(account, jid)
1398 if not contact:
1399 # idle draw or just removed SelfContact
1400 return
1402 family = gajim.contacts.get_metacontacts_family(account, jid)
1403 if family:
1404 # There might be a new big brother
1405 self._recalibrate_metacontact_family(family, account)
1406 self.draw_contact(jid, account)
1407 self.draw_account(account)
1409 for group in contact.get_shown_groups():
1410 self.draw_group(group, account)
1411 self._adjust_group_expand_collapse_state(group, account)
1413 def _idle_draw_jids_of_account(self, jids, account):
1414 '''Draw given contacts and their avatars in a lazy fashion.
1416 Keyword arguments:
1417 jids -- a list of jids to draw
1418 account -- the corresponding account
1420 def _draw_all_contacts(jids, account):
1421 for jid in jids:
1422 family = gajim.contacts.get_metacontacts_family(account, jid)
1423 if family:
1424 # For metacontacts over several accounts:
1425 # When we connect a new account existing brothers
1426 # must be redrawn (got removed and readded)
1427 for data in family:
1428 self.draw_completely(data['jid'], data['account'])
1429 else:
1430 self.draw_completely(jid, account)
1431 yield True
1432 yield False
1434 task = _draw_all_contacts(jids, account)
1435 gobject.idle_add(task.next)
1437 def setup_and_draw_roster(self):
1438 '''create new empty model and draw roster'''
1439 self.modelfilter = None
1440 # (icon, name, type, jid, account, editable, mood_pixbuf,
1441 # activity_pixbuf, tune_pixbuf avatar_pixbuf, padlock_pixbuf)
1442 self.model = gtk.TreeStore(gtk.Image, str, str, str, str,
1443 gtk.gdk.Pixbuf, gtk.gdk.Pixbuf, gtk.gdk.Pixbuf,
1444 gtk.gdk.Pixbuf, gtk.gdk.Pixbuf)
1446 self.model.set_sort_func(1, self._compareIters)
1447 self.model.set_sort_column_id(1, gtk.SORT_ASCENDING)
1448 self.modelfilter = self.model.filter_new()
1449 self.modelfilter.set_visible_func(self._visible_func)
1450 self.modelfilter.connect('row-has-child-toggled',
1451 self.on_modelfilter_row_has_child_toggled)
1452 self.tree.set_model(self.modelfilter)
1454 for acct in gajim.connections:
1455 self.add_account(acct)
1456 self.add_account_contacts(acct)
1457 # Recalculate column width for ellipsizing
1458 self.tree.columns_autosize()
1461 def select_contact(self, jid, account):
1462 '''Select contact in roster. If contact is hidden but has events,
1463 show him.'''
1464 # Refiltering SHOULD NOT be needed:
1465 # When a contact gets a new event he will be redrawn and his
1466 # icon changes, so _visible_func WILL be called on him anyway
1467 iters = self._get_contact_iter(jid, account)
1468 if not iters:
1469 # Not visible in roster
1470 return
1471 path = self.modelfilter.get_path(iters[0])
1472 if self.dragging or not gajim.config.get('scroll_roster_to_last_message'):
1473 # do not change selection while DND'ing
1474 return
1475 # Expand his parent, so this path is visible, don't expand it.
1476 self.tree.expand_to_path(path[:-1])
1477 self.tree.scroll_to_cell(path)
1478 self.tree.set_cursor(path)
1481 def _adjust_account_expand_collapse_state(self, account):
1482 '''Expand/collapse account row based on self.collapsed_rows'''
1483 iterA = self._get_account_iter(account)
1484 if not iterA:
1485 # thank you modelfilter
1486 return
1487 path = self.modelfilter.get_path(iterA)
1488 if account in self.collapsed_rows:
1489 self.tree.collapse_row(path)
1490 else:
1491 self.tree.expand_row(path, False)
1492 return False
1495 def _adjust_group_expand_collapse_state(self, group, account):
1496 '''Expand/collapse group row based on self.collapsed_rows'''
1497 iterG = self._get_group_iter(group, account)
1498 if not iterG:
1499 # Group not visible
1500 return
1501 path = self.modelfilter.get_path(iterG)
1502 if account + group in self.collapsed_rows:
1503 self.tree.collapse_row(path)
1504 else:
1505 self.tree.expand_row(path, False)
1506 return False
1508 ##############################################################################
1509 ### Roster and Modelfilter handling
1510 ##############################################################################
1512 def _search_roster_func(self, model, column, key, titer):
1513 key = key.decode('utf-8').lower()
1514 name = model[titer][C_NAME].decode('utf-8').lower()
1515 return not (key in name)
1517 def refilter_shown_roster_items(self):
1518 self.filtering = True
1519 self.modelfilter.refilter()
1520 self.filtering = False
1522 def contact_has_pending_roster_events(self, contact, account):
1523 '''Return True if the contact or one if it resources has pending events'''
1524 # jid has pending events
1525 if gajim.events.get_nb_roster_events(account, contact.jid) > 0:
1526 return True
1527 # check events of all resources
1528 for contact_ in gajim.contacts.get_contacts(account, contact.jid):
1529 if contact_.resource and gajim.events.get_nb_roster_events(account,
1530 contact_.get_full_jid()) > 0:
1531 return True
1532 return False
1534 def contact_is_visible(self, contact, account):
1535 if self.contact_has_pending_roster_events(contact, account):
1536 return True
1538 if contact.show in ('offline', 'error'):
1539 if contact.jid in gajim.to_be_removed[account]:
1540 return True
1541 return False
1542 if gajim.config.get('show_only_chat_and_online') and contact.show in (
1543 'away', 'xa', 'busy'):
1544 return False
1545 return True
1547 def _visible_func(self, model, titer):
1548 '''Determine whether iter should be visible in the treeview'''
1549 type_ = model[titer][C_TYPE]
1550 if not type_:
1551 return False
1552 if type_ == 'account':
1553 # Always show account
1554 return True
1556 account = model[titer][C_ACCOUNT]
1557 if not account:
1558 return False
1560 account = account.decode('utf-8')
1561 jid = model[titer][C_JID]
1562 if not jid:
1563 return False
1564 jid = jid.decode('utf-8')
1565 if type_ == 'group':
1566 group = jid
1567 if group == _('Transports'):
1568 if self.regroup:
1569 accounts = gajim.contacts.get_accounts()
1570 else:
1571 accounts = [account]
1572 for _acc in accounts:
1573 for contact in gajim.contacts.iter_contacts(_acc):
1574 if group in contact.get_shown_groups() and \
1575 self.contact_has_pending_roster_events(contact, _acc):
1576 return True
1577 return gajim.config.get('show_transports_group') and \
1578 (gajim.account_is_connected(account) or \
1579 gajim.config.get('showoffline'))
1580 if gajim.config.get('showoffline'):
1581 return True
1584 if self.regroup:
1585 # C_ACCOUNT for groups depends on the order
1586 # accounts were connected
1587 # Check all accounts for online group contacts
1588 accounts = gajim.contacts.get_accounts()
1589 else:
1590 accounts = [account]
1591 for _acc in accounts:
1592 for contact in gajim.contacts.iter_contacts(_acc):
1593 # Is this contact in this group ? (last part of if check if it's
1594 # self contact)
1595 if group in contact.get_shown_groups():
1596 if self.contact_is_visible(contact, _acc):
1597 return True
1598 return False
1599 if type_ == 'contact':
1600 if gajim.config.get('showoffline'):
1601 return True
1602 bb_jid = None
1603 bb_account = None
1604 family = gajim.contacts.get_metacontacts_family(account, jid)
1605 if family:
1606 nearby_family, bb_jid, bb_account = \
1607 self._get_nearby_family_and_big_brother(family, account)
1608 if (bb_jid, bb_account) == (jid, account):
1609 # Show the big brother if a child has pending events
1610 for data in nearby_family:
1611 jid = data['jid']
1612 account = data['account']
1613 contact = gajim.contacts.get_contact_with_highest_priority(
1614 account, jid)
1615 if contact and self.contact_is_visible(contact, account):
1616 return True
1617 return False
1618 else:
1619 contact = gajim.contacts.get_contact_with_highest_priority(account,
1620 jid)
1621 return self.contact_is_visible(contact, account)
1622 if type_ == 'agent':
1623 contact = gajim.contacts.get_contact_with_highest_priority(account,
1624 jid)
1625 return self.contact_has_pending_roster_events(contact, account) or \
1626 (gajim.config.get('show_transports_group') and \
1627 (gajim.account_is_connected(account) or \
1628 gajim.config.get('showoffline')))
1629 return True
1631 def _compareIters(self, model, iter1, iter2, data=None):
1632 '''Compare two iters to sort them'''
1633 name1 = model[iter1][C_NAME]
1634 name2 = model[iter2][C_NAME]
1635 if not name1 or not name2:
1636 return 0
1637 name1 = name1.decode('utf-8')
1638 name2 = name2.decode('utf-8')
1639 type1 = model[iter1][C_TYPE]
1640 type2 = model[iter2][C_TYPE]
1641 if type1 == 'self_contact':
1642 return -1
1643 if type2 == 'self_contact':
1644 return 1
1645 if type1 == 'group':
1646 name1 = model[iter1][C_JID]
1647 name2 = model[iter2][C_JID]
1648 if name1 == _('Transports'):
1649 return 1
1650 if name2 == _('Transports'):
1651 return -1
1652 if name1 == _('Not in Roster'):
1653 return 1
1654 if name2 == _('Not in Roster'):
1655 return -1
1656 if name1 == _('Groupchats'):
1657 return 1
1658 if name2 == _('Groupchats'):
1659 return -1
1660 account1 = model[iter1][C_ACCOUNT]
1661 account2 = model[iter2][C_ACCOUNT]
1662 if not account1 or not account2:
1663 return 0
1664 account1 = account1.decode('utf-8')
1665 account2 = account2.decode('utf-8')
1666 if type1 == 'account':
1667 return locale.strcoll(account1, account2)
1668 jid1 = model[iter1][C_JID].decode('utf-8')
1669 jid2 = model[iter2][C_JID].decode('utf-8')
1670 if type1 == 'contact':
1671 lcontact1 = gajim.contacts.get_contacts(account1, jid1)
1672 contact1 = gajim.contacts.get_first_contact_from_jid(account1, jid1)
1673 if not contact1:
1674 return 0
1675 name1 = contact1.get_shown_name()
1676 if type2 == 'contact':
1677 lcontact2 = gajim.contacts.get_contacts(account2, jid2)
1678 contact2 = gajim.contacts.get_first_contact_from_jid(account2, jid2)
1679 if not contact2:
1680 return 0
1681 name2 = contact2.get_shown_name()
1682 # We first compare by show if sort_by_show_in_roster is True or if it's a
1683 # child contact
1684 if type1 == 'contact' and type2 == 'contact' and \
1685 gajim.config.get('sort_by_show_in_roster'):
1686 cshow = {'chat':0, 'online': 1, 'away': 2, 'xa': 3, 'dnd': 4,
1687 'invisible': 5, 'offline': 6, 'not in roster': 7, 'error': 8}
1688 s = self.get_show(lcontact1)
1689 show1 = cshow.get(s, 9)
1690 s = self.get_show(lcontact2)
1691 show2 = cshow.get(s, 9)
1692 removing1 = False
1693 removing2 = False
1694 if show1 == 6 and jid1 in gajim.to_be_removed[account1]:
1695 removing1 = True
1696 if show2 == 6 and jid2 in gajim.to_be_removed[account2]:
1697 removing2 = True
1698 if removing1 and not removing2:
1699 return 1
1700 if removing2 and not removing1:
1701 return -1
1702 sub1 = contact1.sub
1703 sub2 = contact2.sub
1704 # none and from goes after
1705 if sub1 not in ['none', 'from'] and sub2 in ['none', 'from']:
1706 return -1
1707 if sub1 in ['none', 'from'] and sub2 not in ['none', 'from']:
1708 return 1
1709 if show1 < show2:
1710 return -1
1711 elif show1 > show2:
1712 return 1
1713 # We compare names
1714 cmp_result = locale.strcoll(name1.lower(), name2.lower())
1715 if cmp_result < 0:
1716 return -1
1717 if cmp_result > 0:
1718 return 1
1719 if type1 == 'contact' and type2 == 'contact':
1720 # We compare account names
1721 cmp_result = locale.strcoll(account1.lower(), account2.lower())
1722 if cmp_result < 0:
1723 return -1
1724 if cmp_result > 0:
1725 return 1
1726 # We compare jids
1727 cmp_result = locale.strcoll(jid1.lower(), jid2.lower())
1728 if cmp_result < 0:
1729 return -1
1730 if cmp_result > 0:
1731 return 1
1732 return 0
1734 ################################################################################
1735 ### FIXME: Methods that don't belong to roster window...
1736 ### ... atleast not in there current form
1737 ################################################################################
1739 def fire_up_unread_messages_events(self, account):
1740 '''reads from db the unread messages, and fire them up, and
1741 if we find very old unread messages, delete them from unread table'''
1742 results = gajim.logger.get_unread_msgs()
1743 for result in results:
1744 jid = result[4]
1745 shown = result[5]
1746 if gajim.contacts.get_first_contact_from_jid(account, jid) and not \
1747 shown:
1748 # We have this jid in our contacts list
1749 # XXX unread messages should probably have their session saved with
1750 # them
1751 session = gajim.connections[account].make_new_session(jid)
1753 tim = time.localtime(float(result[2]))
1754 session.roster_message(jid, result[1], tim, msg_type='chat',
1755 msg_id=result[0])
1756 gajim.logger.set_shown_unread_msgs(result[0])
1758 elif (time.time() - result[2]) > 2592000:
1759 # ok, here we see that we have a message in unread messages table
1760 # that is older than a month. It is probably from someone not in our
1761 # roster for accounts we usually launch, so we will delete this id
1762 # from unread message tables.
1763 gajim.logger.set_read_messages([result[0]])
1765 def fill_contacts_and_groups_dicts(self, array, account):
1766 '''fill gajim.contacts and gajim.groups'''
1767 # FIXME: This function needs to be splitted
1768 # Most of the logic SHOULD NOT be done at GUI level
1769 if account not in gajim.contacts.get_accounts():
1770 gajim.contacts.add_account(account)
1771 if account not in gajim.groups:
1772 gajim.groups[account] = {}
1773 if gajim.config.get('show_self_contact') == 'always':
1774 self_jid = gajim.get_jid_from_account(account)
1775 if gajim.connections[account].server_resource:
1776 self_jid += '/' + gajim.connections[account].server_resource
1777 array[self_jid] = {'name': gajim.nicks[account],
1778 'groups': ['self_contact'], 'subscription': 'both', 'ask': 'none'}
1779 # .keys() is needed
1780 for jid in array.keys():
1781 # Remove the contact in roster. It might has changed
1782 self.remove_contact(jid, account, force=True)
1783 # Remove old Contact instances
1784 gajim.contacts.remove_jid(account, jid, remove_meta=False)
1785 jids = jid.split('/')
1786 # get jid
1787 ji = jids[0]
1788 # get resource
1789 resource = ''
1790 if len(jids) > 1:
1791 resource = '/'.join(jids[1:])
1792 # get name
1793 name = array[jid]['name'] or ''
1794 show = 'offline' # show is offline by default
1795 status = '' # no status message by default
1797 keyID = ''
1798 attached_keys = gajim.config.get_per('accounts', account,
1799 'attached_gpg_keys').split()
1800 if jid in attached_keys:
1801 keyID = attached_keys[attached_keys.index(jid) + 1]
1803 if gajim.jid_is_transport(jid):
1804 array[jid]['groups'] = [_('Transports')]
1805 contact1 = gajim.contacts.create_contact(jid=ji, name=name,
1806 groups=array[jid]['groups'], show=show, status=status,
1807 sub=array[jid]['subscription'], ask=array[jid]['ask'],
1808 resource=resource, keyID=keyID)
1809 gajim.contacts.add_contact(account, contact1)
1811 if gajim.config.get('ask_avatars_on_startup'):
1812 pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(ji)
1813 if pixbuf == 'ask':
1814 transport = gajim.get_transport_name_from_jid(contact1.jid)
1815 if not transport or gajim.jid_is_transport(contact1.jid):
1816 jid_with_resource = contact1.jid
1817 if contact1.resource:
1818 jid_with_resource += '/' + contact1.resource
1819 gajim.connections[account].request_vcard(jid_with_resource)
1820 else:
1821 host = gajim.get_server_from_jid(contact1.jid)
1822 if host not in gajim.transport_avatar[account]:
1823 gajim.transport_avatar[account][host] = [contact1.jid]
1824 else:
1825 gajim.transport_avatar[account][host].append(contact1.jid)
1827 # If we already have chat windows opened, update them with new contact
1828 # instance
1829 chat_control = gajim.interface.msg_win_mgr.get_control(ji, account)
1830 if chat_control:
1831 chat_control.contact = contact1
1833 def connected_rooms(self, account):
1834 if account in gajim.gc_connected[account].values():
1835 return True
1836 return False
1838 def on_event_removed(self, event_list):
1839 '''Remove contacts on last events removed.
1841 Only performed if removal was requested before but the contact
1842 still had pending events
1844 contact_list = ((event.jid.split('/')[0], event.account) for event in \
1845 event_list)
1847 for jid, account in contact_list:
1848 self.draw_contact(jid, account)
1849 # Remove contacts in roster if removal was requested
1850 key = (jid, account)
1851 if key in self.contacts_to_be_removed.keys():
1852 backend = self.contacts_to_be_removed[key]['backend']
1853 del self.contacts_to_be_removed[key]
1854 # Remove contact will delay removal if there are more events pending
1855 self.remove_contact(jid, account, backend=backend)
1856 self.show_title()
1858 def open_event(self, account, jid, event):
1859 '''If an event was handled, return True, else return False'''
1860 data = event.parameters
1861 ft = gajim.interface.instances['file_transfers']
1862 event = gajim.events.get_first_event(account, jid, event.type_)
1863 if event.type_ == 'normal':
1864 dialogs.SingleMessageWindow(account, jid,
1865 action='receive', from_whom=jid, subject=data[1], message=data[0],
1866 resource=data[5], session=data[8], form_node=data[9])
1867 gajim.events.remove_events(account, jid, event)
1868 return True
1869 elif event.type_ == 'file-request':
1870 contact = gajim.contacts.get_contact_with_highest_priority(account,
1871 jid)
1872 ft.show_file_request(account, contact, data)
1873 gajim.events.remove_events(account, jid, event)
1874 return True
1875 elif event.type_ in ('file-request-error', 'file-send-error'):
1876 ft.show_send_error(data)
1877 gajim.events.remove_events(account, jid, event)
1878 return True
1879 elif event.type_ in ('file-error', 'file-stopped'):
1880 msg_err = ''
1881 if data['error'] == -1:
1882 msg_err = _('Remote contact stopped transfer')
1883 elif data['error'] == -6:
1884 msg_err = _('Error opening file')
1885 ft.show_stopped(jid, data, error_msg=msg_err)
1886 gajim.events.remove_events(account, jid, event)
1887 return True
1888 elif event.type_ == 'file-completed':
1889 ft.show_completed(jid, data)
1890 gajim.events.remove_events(account, jid, event)
1891 return True
1892 elif event.type_ == 'gc-invitation':
1893 dialogs.InvitationReceivedDialog(account, data[0], jid, data[2],
1894 data[1])
1895 gajim.events.remove_events(account, jid, event)
1896 return True
1897 elif event.type_ == 'subscription_request':
1898 dialogs.SubscriptionRequestWindow(jid, data[0], account, data[1])
1899 gajim.events.remove_events(account, jid, event)
1900 return True
1901 elif event.type_ == 'unsubscribed':
1902 gajim.interface.show_unsubscribed_dialog(account, data)
1903 gajim.events.remove_events(account, jid, event)
1904 return True
1905 return False
1907 ################################################################################
1908 ### This and that... random.
1909 ################################################################################
1911 def show_roster_vbox(self, active):
1912 if active:
1913 self.xml.get_widget('roster_vbox2').show()
1914 else:
1915 self.xml.get_widget('roster_vbox2').hide()
1918 def show_tooltip(self, contact):
1919 pointer = self.tree.get_pointer()
1920 props = self.tree.get_path_at_pos(pointer[0], pointer[1])
1921 # check if the current pointer is at the same path
1922 # as it was before setting the timeout
1923 if props and self.tooltip.id == props[0]:
1924 # bounding rectangle of coordinates for the cell within the treeview
1925 rect = self.tree.get_cell_area(props[0], props[1])
1927 # position of the treeview on the screen
1928 position = self.tree.window.get_origin()
1929 self.tooltip.show_tooltip(contact, rect.height, position[1] + rect.y)
1930 else:
1931 self.tooltip.hide_tooltip()
1934 def authorize(self, widget, jid, account):
1935 '''Authorize a contact (by re-sending auth menuitem)'''
1936 gajim.connections[account].send_authorization(jid)
1937 dialogs.InformationDialog(_('Authorization has been sent'),
1938 _('Now "%s" will know your status.') %jid)
1940 def req_sub(self, widget, jid, txt, account, groups=[], nickname=None,
1941 auto_auth=False):
1942 '''Request subscription to a contact'''
1943 gajim.connections[account].request_subscription(jid, txt, nickname,
1944 groups, auto_auth, gajim.nicks[account])
1945 contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
1946 if not contact:
1947 keyID = ''
1948 attached_keys = gajim.config.get_per('accounts', account,
1949 'attached_gpg_keys').split()
1950 if jid in attached_keys:
1951 keyID = attached_keys[attached_keys.index(jid) + 1]
1952 contact = gajim.contacts.create_contact(jid=jid, name=nickname,
1953 groups=groups, show='requested', status='', ask='none',
1954 sub='subscribe', keyID=keyID)
1955 gajim.contacts.add_contact(account, contact)
1956 else:
1957 if not _('Not in Roster') in contact.get_shown_groups():
1958 dialogs.InformationDialog(_('Subscription request has been sent'),
1959 _('If "%s" accepts this request you will know his or her status.'
1960 ) % jid)
1961 return
1962 self.remove_contact(contact.jid, account, force=True)
1963 contact.groups = groups
1964 if nickname:
1965 contact.name = nickname
1966 self.add_contact(jid, account)
1968 def revoke_auth(self, widget, jid, account):
1969 '''Revoke a contact's authorization'''
1970 gajim.connections[account].refuse_authorization(jid)
1971 dialogs.InformationDialog(_('Authorization has been removed'),
1972 _('Now "%s" will always see you as offline.') %jid)
1974 def set_state(self, account, state):
1975 child_iterA = self._get_account_iter(account, self.model)
1976 if child_iterA:
1977 self.model[child_iterA][0] = \
1978 gajim.interface.jabber_state_images['16'][state]
1979 if gajim.interface.systray_enabled:
1980 gajim.interface.systray.change_status(state)
1982 def set_connecting_state(self, account):
1983 self.set_state(account, 'connecting')
1985 def send_status(self, account, status, txt, auto=False, to=None):
1986 child_iterA = self._get_account_iter(account, self.model)
1987 if status != 'offline':
1988 if to is None:
1989 if status == gajim.connections[account].get_status() and \
1990 txt == gajim.connections[account].status:
1991 return
1992 gajim.config.set_per('accounts', account, 'last_status', status)
1993 gajim.config.set_per('accounts', account, 'last_status_msg',
1994 helpers.to_one_line(txt))
1995 if gajim.connections[account].connected < 2:
1996 self.set_connecting_state(account)
1998 keyid = gajim.config.get_per('accounts', account, 'keyid')
1999 if keyid and not gajim.connections[account].gpg:
2000 dialogs.WarningDialog(_('GPG is not usable'),
2001 _('You will be connected to %s without OpenPGP.') % account)
2003 self.send_status_continue(account, status, txt, auto, to)
2005 def send_pep(self, account, pep_dict=None):
2006 '''Sends pep information (activity, mood)'''
2007 if not pep_dict:
2008 return
2009 # activity
2010 if 'activity' in pep_dict and pep_dict['activity'] in pep.ACTIVITIES:
2011 activity = pep_dict['activity']
2012 if 'subactivity' in pep_dict and \
2013 pep_dict['subactivity'] in pep.ACTIVITIES[activity]:
2014 subactivity = pep_dict['subactivity']
2015 else:
2016 subactivity = 'other'
2017 if 'activity_text' in pep_dict:
2018 activity_text = pep_dict['activity_text']
2019 else:
2020 activity_text = ''
2021 pep.user_send_activity(account, activity, subactivity, activity_text)
2022 else:
2023 pep.user_send_activity(account, '')
2025 # mood
2026 if 'mood' in pep_dict and pep_dict['mood'] in pep.MOODS:
2027 mood = pep_dict['mood']
2028 if 'mood_text' in pep_dict:
2029 mood_text = pep_dict['mood_text']
2030 else:
2031 mood_text = ''
2032 pep.user_send_mood(account, mood, mood_text)
2033 else:
2034 pep.user_send_mood(account, '')
2036 def send_status_continue(self, account, status, txt, auto, to):
2037 if gajim.account_is_connected(account) and not to:
2038 if status == 'online' and gajim.interface.sleeper.getState() != \
2039 common.sleepy.STATE_UNKNOWN:
2040 gajim.sleeper_state[account] = 'online'
2041 elif gajim.sleeper_state[account] not in ('autoaway', 'autoxa') or \
2042 status == 'offline':
2043 gajim.sleeper_state[account] = 'off'
2045 if to:
2046 gajim.connections[account].send_custom_status(status, txt, to)
2047 else:
2048 if status in ('invisible', 'offline'):
2049 pep.delete_pep(gajim.get_jid_from_account(account), \
2050 account)
2051 was_invisible = gajim.connections[account].connected == \
2052 gajim.SHOW_LIST.index('invisible')
2053 gajim.connections[account].change_status(status, txt, auto)
2055 if account in gajim.interface.status_sent_to_users:
2056 gajim.interface.status_sent_to_users[account] = {}
2057 if account in gajim.interface.status_sent_to_groups:
2058 gajim.interface.status_sent_to_groups[account] = {}
2059 for gc_control in gajim.interface.msg_win_mgr.get_controls(
2060 message_control.TYPE_GC) + \
2061 gajim.interface.minimized_controls[account].values():
2062 if gc_control.account == account:
2063 if gajim.gc_connected[account][gc_control.room_jid]:
2064 gajim.connections[account].send_gc_status(gc_control.nick,
2065 gc_control.room_jid, status, txt)
2066 else:
2067 # for some reason, we are not connected to the room even if
2068 # tab is opened, send initial join_gc()
2069 gajim.connections[account].join_gc(gc_control.nick,
2070 gc_control.room_jid, None)
2071 if was_invisible and status != 'offline':
2072 # We come back from invisible, join bookmarks
2073 gajim.interface.auto_join_bookmarks(account)
2076 def chg_contact_status(self, contact, show, status, account):
2077 '''When a contact changes his or her status'''
2078 contact_instances = gajim.contacts.get_contacts(account, contact.jid)
2079 contact.show = show
2080 contact.status = status
2081 # name is to show in conversation window
2082 name = contact.get_shown_name()
2083 fjid = contact.get_full_jid()
2085 # The contact has several resources
2086 if len(contact_instances) > 1:
2087 if contact.resource != '':
2088 name += '/' + contact.resource
2090 # Remove resource when going offline
2091 if show in ('offline', 'error') and \
2092 not self.contact_has_pending_roster_events(contact, account):
2093 ctrl = gajim.interface.msg_win_mgr.get_control(fjid, account)
2094 if ctrl:
2095 ctrl.update_ui()
2096 ctrl.parent_win.redraw_tab(ctrl)
2097 # keep the contact around, since it's
2098 # already attached to the control
2099 else:
2100 gajim.contacts.remove_contact(account, contact)
2102 elif contact.jid == gajim.get_jid_from_account(account) and \
2103 show in ('offline', 'error'):
2104 if gajim.config.get('show_self_contact') != 'never':
2105 # SelfContact went offline. Remove him when last pending
2106 # message was read
2107 self.remove_contact(contact.jid, account, backend=True)
2109 uf_show = helpers.get_uf_show(show)
2111 # print status in chat window and update status/GPG image
2112 ctrl = gajim.interface.msg_win_mgr.get_control(contact.jid, account)
2113 if ctrl and ctrl.type_id != message_control.TYPE_GC:
2114 ctrl.contact = gajim.contacts.get_contact_with_highest_priority(
2115 account, contact.jid)
2116 ctrl.update_status_display(name, uf_show, status)
2118 if contact.resource:
2119 ctrl = gajim.interface.msg_win_mgr.get_control(fjid, account)
2120 if ctrl:
2121 ctrl.update_status_display(name, uf_show, status)
2123 # Delete pep if needed
2124 keep_pep = any(c.show not in ('error', 'offline') for c in
2125 contact_instances)
2126 if not keep_pep and contact.jid != gajim.get_jid_from_account(account) \
2127 and not contact.is_groupchat():
2128 pep.delete_pep(contact.jid, account)
2130 # Redraw everything and select the sender
2131 self.adjust_and_draw_contact_context(contact.jid, account)
2134 def on_status_changed(self, account, show):
2135 '''the core tells us that our status has changed'''
2136 if account not in gajim.contacts.get_accounts():
2137 return
2138 child_iterA = self._get_account_iter(account, self.model)
2139 if gajim.config.get('show_self_contact') == 'always':
2140 self_resource = gajim.connections[account].server_resource
2141 self_contact = gajim.contacts.get_contact(account,
2142 gajim.get_jid_from_account(account), resource=self_resource)
2143 if self_contact:
2144 status = gajim.connections[account].status
2145 self.chg_contact_status(self_contact, show, status, account)
2146 self.set_account_status_icon(account)
2147 if show == 'offline':
2148 if self.quit_on_next_offline > -1:
2149 # we want to quit, we are waiting for all accounts to be offline
2150 self.quit_on_next_offline -= 1
2151 if self.quit_on_next_offline < 1:
2152 # all accounts offline, quit
2153 self.quit_gtkgui_interface()
2154 else:
2155 # No need to redraw contacts if we're quitting
2156 if child_iterA:
2157 self.model[child_iterA][C_AVATAR_PIXBUF] = None
2158 if account in gajim.con_types:
2159 gajim.con_types[account] = None
2160 for jid in gajim.contacts.get_jid_list(account):
2161 lcontact = gajim.contacts.get_contacts(account, jid)
2162 ctrl = gajim.interface.msg_win_mgr.get_gc_control(jid, account)
2163 for contact in [c for c in lcontact if ((c.show != 'offline' or \
2164 c.is_transport()) and not ctrl)]:
2165 self.chg_contact_status(contact, 'offline', '', account)
2166 self.actions_menu_needs_rebuild = True
2167 self.update_status_combobox()
2169 def get_status_message(self, show, on_response, show_pep=True,
2170 always_ask=False):
2171 ''' get the status message by:
2172 1/ looking in default status message
2173 2/ asking to user if needed depending on ask_on(ff)line_status and
2174 always_ask
2175 show_pep can be False to hide pep things from status message or True
2177 empty_pep = {'activity': '', 'subactivity': '', 'activity_text': '',
2178 'mood': '', 'mood_text': ''}
2179 if show in gajim.config.get_per('defaultstatusmsg'):
2180 if gajim.config.get_per('defaultstatusmsg', show, 'enabled'):
2181 msg = gajim.config.get_per('defaultstatusmsg', show, 'message')
2182 msg = helpers.from_one_line(msg)
2183 on_response(msg, empty_pep)
2184 return
2185 if not always_ask and ((show == 'online' and not gajim.config.get(
2186 'ask_online_status')) or (show in ('offline', 'invisible') and not \
2187 gajim.config.get('ask_offline_status'))):
2188 on_response('', empty_pep)
2189 return
2191 dlg = dialogs.ChangeStatusMessageDialog(on_response, show, show_pep)
2192 dlg.dialog.present() # show it on current workspace
2194 def change_status(self, widget, account, status):
2195 def change(account, status):
2196 def on_response(message, pep_dict):
2197 if message is None:
2198 # user pressed Cancel to change status message dialog
2199 return
2200 self.send_status(account, status, message)
2201 self.send_pep(account, pep_dict)
2202 self.get_status_message(status, on_response)
2204 if status == 'invisible' and self.connected_rooms(account):
2205 dialogs.ConfirmationDialog(
2206 _('You are participating in one or more group chats'),
2207 _('Changing your status to invisible will result in disconnection '
2208 'from those group chats. Are you sure you want to go invisible?'),
2209 on_response_ok = (change, account, status))
2210 else:
2211 change(account, status)
2213 def update_status_combobox(self):
2214 # table to change index in connection.connected to index in combobox
2215 table = {'offline':9, 'connecting':9, 'online':0, 'chat':1, 'away':2,
2216 'xa':3, 'dnd':4, 'invisible':5}
2218 # we check if there are more options in the combobox that it should
2219 # if yes, we remove the first ones
2220 while len(self.status_combobox.get_model()) > len(table)+2:
2221 self.status_combobox.remove_text(0)
2223 show = helpers.get_global_show()
2224 # temporarily block signal in order not to send status that we show
2225 # in the combobox
2226 self.combobox_callback_active = False
2227 if helpers.statuses_unified():
2228 self.status_combobox.set_active(table[show])
2229 else:
2230 uf_show = helpers.get_uf_show(show)
2231 liststore = self.status_combobox.get_model()
2232 liststore.prepend(['SEPARATOR', None, '', True])
2233 status_combobox_text = uf_show + ' (' + _("desync'ed") +')'
2234 liststore.prepend([status_combobox_text,
2235 gajim.interface.jabber_state_images['16'][show], show, False])
2236 self.status_combobox.set_active(0)
2237 gajim.interface._change_awn_icon_status(show)
2238 self.combobox_callback_active = True
2239 if gajim.interface.systray_enabled:
2240 gajim.interface.systray.change_status(show)
2242 def get_show(self, lcontact):
2243 prio = lcontact[0].priority
2244 show = lcontact[0].show
2245 for u in lcontact:
2246 if u.priority > prio:
2247 prio = u.priority
2248 show = u.show
2249 return show
2251 def on_message_window_delete(self, win_mgr, msg_win):
2252 if gajim.config.get('one_message_window') == 'always_with_roster':
2253 self.show_roster_vbox(True)
2254 gtkgui_helpers.resize_window(self.window,
2255 gajim.config.get('roster_width'),
2256 gajim.config.get('roster_height'))
2258 def close_all_from_dict(self, dic):
2259 '''close all the windows in the given dictionary'''
2260 for w in dic.values():
2261 if isinstance(w, dict):
2262 self.close_all_from_dict(w)
2263 else:
2264 w.window.destroy()
2266 def close_all(self, account, force=False):
2267 '''close all the windows from an account
2268 if force is True, do not ask confirmation before closing chat/gc windows
2270 if account in gajim.interface.instances:
2271 self.close_all_from_dict(gajim.interface.instances[account])
2272 for ctrl in gajim.interface.msg_win_mgr.get_controls(acct=account):
2273 ctrl.parent_win.remove_tab(ctrl, ctrl.parent_win.CLOSE_CLOSE_BUTTON,
2274 force = force)
2276 def on_roster_window_delete_event(self, widget, event):
2277 '''Main window X button was clicked'''
2278 if gajim.interface.systray_enabled and not gajim.config.get(
2279 'quit_on_roster_x_button') and gajim.config.get('trayicon') != 'on_event':
2280 self.tooltip.hide_tooltip()
2281 self.window.hide()
2282 elif gajim.config.get('quit_on_roster_x_button'):
2283 self.on_quit_request()
2284 else:
2285 def on_ok(checked):
2286 if checked:
2287 gajim.config.set('quit_on_roster_x_button', True)
2288 self.on_quit_request()
2289 dialogs.ConfirmationDialogCheck(_('Really quit Gajim?'),
2290 _('Are you sure you want to quit Gajim?'),
2291 _('Always close Gajim'), on_response_ok=on_ok)
2292 return True # do NOT destroy the window
2294 def prepare_quit(self):
2295 msgwin_width_adjust = 0
2297 # in case show_roster_on_start is False and roster is never shown
2298 # window.window is None
2299 if self.window.window is not None:
2300 x, y = self.window.window.get_root_origin()
2301 gajim.config.set('roster_x-position', x)
2302 gajim.config.set('roster_y-position', y)
2303 width, height = self.window.get_size()
2304 # For the width use the size of the vbox containing the tree and
2305 # status combo, this will cancel out any hpaned width
2306 width = self.xml.get_widget('roster_vbox2').allocation.width
2307 gajim.config.set('roster_width', width)
2308 gajim.config.set('roster_height', height)
2309 if not self.xml.get_widget('roster_vbox2').get_property('visible'):
2310 # The roster vbox is hidden, so the message window is larger
2311 # then we want to save (i.e. the window will grow every startup)
2312 # so adjust.
2313 msgwin_width_adjust = -1 * width
2314 gajim.config.set('show_roster_on_startup',
2315 self.window.get_property('visible'))
2316 gajim.interface.msg_win_mgr.shutdown(msgwin_width_adjust)
2318 gajim.config.set('collapsed_rows', '\t'.join(self.collapsed_rows))
2319 gajim.interface.save_config()
2320 for account in gajim.connections:
2321 gajim.connections[account].quit(True)
2322 self.close_all(account)
2323 if gajim.interface.systray_enabled:
2324 gajim.interface.hide_systray()
2326 def quit_gtkgui_interface(self):
2327 '''When we quit the gtk interface : exit gtk'''
2328 self.prepare_quit()
2329 gtk.main_quit()
2331 def on_quit_request(self, widget=None):
2332 ''' user want to quit. Check if he should be warned about messages
2333 pending. Terminate all sessions and send offline to all connected
2334 account. We do NOT really quit gajim here '''
2335 accounts = gajim.connections.keys()
2336 get_msg = False
2337 for acct in accounts:
2338 if gajim.connections[acct].connected:
2339 get_msg = True
2340 break
2342 def on_continue2(message, pep_dict):
2343 self.quit_on_next_offline = 0
2344 accounts_to_disconnect = []
2345 for acct in accounts:
2346 if gajim.connections[acct].connected:
2347 self.quit_on_next_offline += 1
2348 accounts_to_disconnect.append(acct)
2350 for acct in accounts_to_disconnect:
2351 self.send_status(acct, 'offline', message)
2352 self.send_pep(acct, pep_dict)
2354 if not self.quit_on_next_offline:
2355 self.quit_gtkgui_interface()
2357 def on_continue(message, pep_dict):
2358 if message is None:
2359 # user pressed Cancel to change status message dialog
2360 return
2361 # check if we have unread messages
2362 unread = gajim.events.get_nb_events()
2363 if not gajim.config.get('notify_on_all_muc_messages'):
2364 unread_not_to_notify = gajim.events.get_nb_events(
2365 ['printed_gc_msg'])
2366 unread -= unread_not_to_notify
2368 # check if we have recent messages
2369 recent = False
2370 for win in gajim.interface.msg_win_mgr.windows():
2371 for ctrl in win.controls():
2372 fjid = ctrl.get_full_jid()
2373 if fjid in gajim.last_message_time[ctrl.account]:
2374 if time.time() - gajim.last_message_time[ctrl.account][fjid] \
2375 < 2:
2376 recent = True
2377 break
2378 if recent:
2379 break
2381 if unread or recent:
2382 dialogs.ConfirmationDialog(_('You have unread messages'),
2383 _('Messages will only be available for reading them later if you'
2384 ' have history enabled and contact is in your roster.'),
2385 on_response_ok=(on_continue2, message, pep_dict))
2386 return
2387 on_continue2(message, pep_dict)
2389 if get_msg:
2390 self.get_status_message('offline', on_continue, show_pep=False)
2391 else:
2392 on_continue('', None)
2394 ################################################################################
2395 ### Menu and GUI callbacks
2396 ### FIXME: order callbacks in itself...
2397 ################################################################################
2399 def on_actions_menuitem_activate(self, widget):
2400 self.make_menu()
2402 def on_edit_menuitem_activate(self, widget):
2403 '''need to call make_menu to build profile, avatar item'''
2404 self.make_menu()
2406 def on_bookmark_menuitem_activate(self, widget, account, bookmark):
2407 gajim.interface.join_gc_room(account, bookmark['jid'], bookmark['nick'],
2408 bookmark['password'])
2410 def on_send_server_message_menuitem_activate(self, widget, account):
2411 server = gajim.config.get_per('accounts', account, 'hostname')
2412 server += '/announce/online'
2413 dialogs.SingleMessageWindow(account, server, 'send')
2415 def on_xml_console_menuitem_activate(self, widget, account):
2416 if 'xml_console' in gajim.interface.instances[account]:
2417 gajim.interface.instances[account]['xml_console'].window.present()
2418 else:
2419 gajim.interface.instances[account]['xml_console'] = \
2420 dialogs.XMLConsoleWindow(account)
2422 def on_privacy_lists_menuitem_activate(self, widget, account):
2423 if 'privacy_lists' in gajim.interface.instances[account]:
2424 gajim.interface.instances[account]['privacy_lists'].window.present()
2425 else:
2426 gajim.interface.instances[account]['privacy_lists'] = \
2427 dialogs.PrivacyListsWindow(account)
2429 def on_set_motd_menuitem_activate(self, widget, account):
2430 server = gajim.config.get_per('accounts', account, 'hostname')
2431 server += '/announce/motd'
2432 dialogs.SingleMessageWindow(account, server, 'send')
2434 def on_update_motd_menuitem_activate(self, widget, account):
2435 server = gajim.config.get_per('accounts', account, 'hostname')
2436 server += '/announce/motd/update'
2437 dialogs.SingleMessageWindow(account, server, 'send')
2439 def on_delete_motd_menuitem_activate(self, widget, account):
2440 server = gajim.config.get_per('accounts', account, 'hostname')
2441 server += '/announce/motd/delete'
2442 gajim.connections[account].send_motd(server)
2444 def on_history_manager_menuitem_activate(self, widget):
2445 if os.name == 'nt':
2446 if os.path.exists('history_manager.exe'): # user is running stable
2447 helpers.exec_command('history_manager.exe')
2448 else: # user is running svn
2449 helpers.exec_command('%s history_manager.py' % sys.executable)
2450 else: # Unix user
2451 helpers.exec_command('%s history_manager.py' % sys.executable)
2453 def on_info(self, widget, contact, account):
2454 '''Call vcard_information_window class to display contact's information'''
2455 if gajim.connections[account].is_zeroconf:
2456 self.on_info_zeroconf(widget, contact, account)
2457 return
2459 info = gajim.interface.instances[account]['infos']
2460 if contact.jid in info:
2461 info[contact.jid].window.present()
2462 else:
2463 info[contact.jid] = vcard.VcardWindow(contact, account)
2465 def on_info_zeroconf(self, widget, contact, account):
2466 info = gajim.interface.instances[account]['infos']
2467 if contact.jid in info:
2468 info[contact.jid].window.present()
2469 else:
2470 contact = gajim.contacts.get_first_contact_from_jid(account,
2471 contact.jid)
2472 if contact.show in ('offline', 'error'):
2473 # don't show info on offline contacts
2474 return
2475 info[contact.jid] = vcard.ZeroconfVcardWindow(contact, account)
2477 def on_roster_treeview_leave_notify_event(self, widget, event):
2478 props = widget.get_path_at_pos(int(event.x), int(event.y))
2479 if self.tooltip.timeout > 0:
2480 if not props or self.tooltip.id == props[0]:
2481 self.tooltip.hide_tooltip()
2483 def on_roster_treeview_motion_notify_event(self, widget, event):
2484 model = widget.get_model()
2485 props = widget.get_path_at_pos(int(event.x), int(event.y))
2486 if self.tooltip.timeout > 0:
2487 if not props or self.tooltip.id != props[0]:
2488 self.tooltip.hide_tooltip()
2489 if props:
2490 row = props[0]
2491 titer = None
2492 try:
2493 titer = model.get_iter(row)
2494 except Exception:
2495 self.tooltip.hide_tooltip()
2496 return
2497 if model[titer][C_TYPE] in ('contact', 'self_contact'):
2498 # we're on a contact entry in the roster
2499 account = model[titer][C_ACCOUNT].decode('utf-8')
2500 jid = model[titer][C_JID].decode('utf-8')
2501 if self.tooltip.timeout == 0 or self.tooltip.id != props[0]:
2502 self.tooltip.id = row
2503 contacts = gajim.contacts.get_contacts(account, jid)
2504 connected_contacts = []
2505 for c in contacts:
2506 if c.show not in ('offline', 'error'):
2507 connected_contacts.append(c)
2508 if not connected_contacts:
2509 # no connected contacts, show the ofline one
2510 connected_contacts = contacts
2511 self.tooltip.account = account
2512 self.tooltip.timeout = gobject.timeout_add(500,
2513 self.show_tooltip, connected_contacts)
2514 elif model[titer][C_TYPE] == 'groupchat':
2515 account = model[titer][C_ACCOUNT].decode('utf-8')
2516 jid = model[titer][C_JID].decode('utf-8')
2517 if self.tooltip.timeout == 0 or self.tooltip.id != props[0]:
2518 self.tooltip.id = row
2519 contact = gajim.contacts.get_contacts(account, jid)
2520 self.tooltip.account = account
2521 self.tooltip.timeout = gobject.timeout_add(500,
2522 self.show_tooltip, contact)
2523 elif model[titer][C_TYPE] == 'account':
2524 # we're on an account entry in the roster
2525 account = model[titer][C_ACCOUNT].decode('utf-8')
2526 if account == 'all':
2527 if self.tooltip.timeout == 0 or self.tooltip.id != props[0]:
2528 self.tooltip.id = row
2529 self.tooltip.account = None
2530 self.tooltip.timeout = gobject.timeout_add(500,
2531 self.show_tooltip, [])
2532 return
2533 jid = gajim.get_jid_from_account(account)
2534 contacts = []
2535 connection = gajim.connections[account]
2536 # get our current contact info
2538 nbr_on, nbr_total = gajim.contacts.get_nb_online_total_contacts(
2539 accounts = [account])
2540 account_name = account
2541 if gajim.account_is_connected(account):
2542 account_name += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total))
2543 contact = gajim.contacts.create_contact(jid=jid, name=account_name,
2544 show=connection.get_status(), sub='', status=connection.status,
2545 resource=connection.server_resource,
2546 priority=connection.priority, mood=connection.mood,
2547 tune=connection.tune, activity=connection.activity)
2548 if gajim.connections[account].gpg:
2549 contact.keyID = gajim.config.get_per('accounts', connection.name,
2550 'keyid')
2551 contacts.append(contact)
2552 # if we're online ...
2553 if connection.connection:
2554 roster = connection.connection.getRoster()
2555 # in threadless connection when no roster stanza is sent,
2556 # 'roster' is None
2557 if roster and roster.getItem(jid):
2558 resources = roster.getResources(jid)
2559 # ...get the contact info for our other online resources
2560 for resource in resources:
2561 # Check if we already have this resource
2562 found = False
2563 for contact_ in contacts:
2564 if contact_.resource == resource:
2565 found = True
2566 break
2567 if found:
2568 continue
2569 show = roster.getShow(jid+'/'+resource)
2570 if not show:
2571 show = 'online'
2572 contact = gajim.contacts.create_contact(jid=jid,
2573 name=account, groups=['self_contact'], show=show,
2574 status=roster.getStatus(jid + '/' + resource),
2575 resource=resource,
2576 priority=roster.getPriority(jid + '/' + resource))
2577 contacts.append(contact)
2578 if self.tooltip.timeout == 0 or self.tooltip.id != props[0]:
2579 self.tooltip.id = row
2580 self.tooltip.account = None
2581 self.tooltip.timeout = gobject.timeout_add(500,
2582 self.show_tooltip, contacts)
2584 def on_agent_logging(self, widget, jid, state, account):
2585 '''When an agent is requested to log in or off'''
2586 gajim.connections[account].send_agent_status(jid, state)
2588 def on_edit_agent(self, widget, contact, account):
2589 '''When we want to modify the agent registration'''
2590 gajim.connections[account].request_register_agent_info(contact.jid)
2592 def on_remove_agent(self, widget, list_):
2593 '''When an agent is requested to be removed. list_ is a list of
2594 (contact, account) tuple'''
2595 for (contact, account) in list_:
2596 if gajim.config.get_per('accounts', account, 'hostname') == \
2597 contact.jid:
2598 # We remove the server contact
2599 # remove it from treeview
2600 gajim.connections[account].unsubscribe(contact.jid)
2601 self.remove_contact(contact.jid, account, backend=True)
2602 return
2604 def remove(list_):
2605 for (contact, account) in list_:
2606 full_jid = contact.get_full_jid()
2607 gajim.connections[account].unsubscribe_agent(full_jid)
2608 # remove transport from treeview
2609 self.remove_contact(contact.jid, account, backend=True)
2611 # Check if there are unread events from some contacts
2612 has_unread_events = False
2613 for (contact, account) in list_:
2614 for jid in gajim.events.get_events(account):
2615 if jid.endswith(contact.jid):
2616 has_unread_events = True
2617 break
2618 if has_unread_events:
2619 dialogs.ErrorDialog(_('You have unread messages'),
2620 _('You must read them before removing this transport.'))
2621 return
2622 if len(list_) == 1:
2623 pritext = _('Transport "%s" will be removed') % list_[0][0].jid
2624 sectext = _('You will no longer be able to send and receive messages '
2625 'from contacts using this transport.')
2626 else:
2627 pritext = _('Transports will be removed')
2628 jids = ''
2629 for (contact, account) in list_:
2630 jids += '\n ' + contact.get_shown_name() + ','
2631 jids = jids[:-1] + '.'
2632 sectext = _('You will no longer be able to send and receive messages '
2633 'to contacts from these transports: %s') % jids
2634 dialogs.ConfirmationDialog(pritext, sectext,
2635 on_response_ok = (remove, list_))
2637 def on_block(self, widget, list_, group=None):
2638 ''' When clicked on the 'block' button in context menu.
2639 list_ is a list of (contact, account)'''
2640 def on_continue(msg, pep_dict):
2641 if msg is None:
2642 # user pressed Cancel to change status message dialog
2643 return
2644 accounts = []
2645 if group is None:
2646 for (contact, account) in list_:
2647 if account not in accounts:
2648 if not gajim.connections[account].privacy_rules_supported:
2649 continue
2650 accounts.append(account)
2651 self.send_status(account, 'offline', msg, to=contact.jid)
2652 new_rule = {'order': u'1', 'type': u'jid', 'action': u'deny',
2653 'value' : contact.jid, 'child': [u'message', u'iq',
2654 u'presence-out']}
2655 gajim.connections[account].blocked_list.append(new_rule)
2656 # needed for draw_contact:
2657 gajim.connections[account].blocked_contacts.append(
2658 contact.jid)
2659 self.draw_contact(contact.jid, account)
2660 else:
2661 for (contact, account) in list_:
2662 if account not in accounts:
2663 if not gajim.connections[account].privacy_rules_supported:
2664 continue
2665 accounts.append(account)
2666 # needed for draw_group:
2667 gajim.connections[account].blocked_groups.append(group)
2668 self.draw_group(group, account)
2669 self.send_status(account, 'offline', msg, to=contact.jid)
2670 self.draw_contact(contact.jid, account)
2671 new_rule = {'order': u'1', 'type': u'group', 'action': u'deny',
2672 'value' : group, 'child': [u'message', u'iq', u'presence-out']}
2673 gajim.connections[account].blocked_list.append(new_rule)
2674 for account in accounts:
2675 connection = gajim.connections[account]
2676 connection.set_privacy_list('block', connection.blocked_list)
2677 if len(connection.blocked_list) == 1:
2678 connection.set_active_list('block')
2679 connection.set_default_list('block')
2680 connection.get_privacy_list('block')
2682 def _block_it(is_checked=None):
2683 if is_checked is not None: # dialog has been shown
2684 if is_checked: # user does not want to be asked again
2685 gajim.config.set('confirm_block', 'no')
2686 else:
2687 gajim.config.set('confirm_block', 'yes')
2688 self.get_status_message('offline', on_continue, show_pep=False)
2690 confirm_block = gajim.config.get('confirm_block')
2691 if confirm_block == 'no':
2692 _block_it()
2693 return
2694 pritext = _('You are about to block a contact. Are you sure you want'
2695 ' to continue?')
2696 sectext = _('This contact will see you offline and you will not receive '
2697 'messages he will send you.')
2698 dlg = dialogs.ConfirmationDialogCheck(pritext, sectext,
2699 _('Do _not ask me again'), on_response_ok=_block_it)
2701 def on_unblock(self, widget, list_, group=None):
2702 ''' When clicked on the 'unblock' button in context menu. '''
2703 accounts = []
2704 if group is None:
2705 for (contact, account) in list_:
2706 if account not in accounts:
2707 if gajim.connections[account].privacy_rules_supported:
2708 accounts.append(account)
2709 gajim.connections[account].new_blocked_list = []
2710 gajim.connections[account].to_unblock = []
2711 gajim.connections[account].to_unblock.append(contact.jid)
2712 else:
2713 gajim.connections[account].to_unblock.append(contact.jid)
2714 # needed for draw_contact:
2715 if contact.jid in gajim.connections[account].blocked_contacts:
2716 gajim.connections[account].blocked_contacts.remove(contact.jid)
2717 self.draw_contact(contact.jid, account)
2718 for account in accounts:
2719 for rule in gajim.connections[account].blocked_list:
2720 if rule['action'] != 'deny' or rule['type'] != 'jid' \
2721 or rule['value'] not in gajim.connections[account].to_unblock:
2722 gajim.connections[account].new_blocked_list.append(rule)
2723 else:
2724 for (contact, account) in list_:
2725 if account not in accounts:
2726 if gajim.connections[account].privacy_rules_supported:
2727 accounts.append(account)
2728 # needed for draw_group:
2729 if group in gajim.connections[account].blocked_groups:
2730 gajim.connections[account].blocked_groups.remove(group)
2731 self.draw_group(group, account)
2732 gajim.connections[account].new_blocked_list = []
2733 for rule in gajim.connections[account].blocked_list:
2734 if rule['action'] != 'deny' or rule['type'] != 'group' \
2735 or rule['value'] != group:
2736 gajim.connections[account].new_blocked_list.append(rule)
2737 self.draw_contact(contact.jid, account)
2738 for account in accounts:
2739 gajim.connections[account].set_privacy_list('block',
2740 gajim.connections[account].new_blocked_list)
2741 gajim.connections[account].get_privacy_list('block')
2742 if len(gajim.connections[account].new_blocked_list) == 0:
2743 gajim.connections[account].blocked_list = []
2744 gajim.connections[account].blocked_contacts = []
2745 gajim.connections[account].blocked_groups = []
2746 gajim.connections[account].set_default_list('')
2747 gajim.connections[account].set_active_list('')
2748 gajim.connections[account].del_privacy_list('block')
2749 if 'blocked_contacts' in gajim.interface.instances[account]:
2750 gajim.interface.instances[account]['blocked_contacts'].\
2751 privacy_list_received([])
2752 for (contact, account) in list_:
2753 if not self.regroup:
2754 show = gajim.SHOW_LIST[gajim.connections[account].connected]
2755 else: # accounts merged
2756 show = helpers.get_global_show()
2757 if show == 'invisible':
2758 # Don't send our presence if we're invisible
2759 continue
2760 if account not in accounts:
2761 accounts.append(account)
2762 if gajim.connections[account].privacy_rules_supported:
2763 self.send_status(account, show,
2764 gajim.connections[account].status, to=contact.jid)
2765 else:
2766 self.send_status(account, show,
2767 gajim.connections[account].status, to=contact.jid)
2769 def on_rename(self, widget, row_type, jid, account):
2770 # this function is called either by F2 or by Rename menuitem
2771 if 'rename' in gajim.interface.instances:
2772 gajim.interface.instances['rename'].dialog.present()
2773 return
2775 # account is offline, don't allow to rename
2776 if gajim.connections[account].connected < 2:
2777 return
2778 if row_type in ('contact', 'agent'):
2779 # it's jid
2780 title = _('Rename Contact')
2781 message = _('Enter a new nickname for contact %s') % jid
2782 old_text = gajim.contacts.get_contact_with_highest_priority(account,
2783 jid).name
2784 elif row_type == 'group':
2785 if jid in helpers.special_groups + (_('General'),):
2786 return
2787 old_text = jid
2788 title = _('Rename Group')
2789 message = _('Enter a new name for group %s') % \
2790 gobject.markup_escape_text(jid)
2792 def on_renamed(new_text, account, row_type, jid, old_text):
2793 if 'rename' in gajim.interface.instances:
2794 del gajim.interface.instances['rename']
2795 if row_type in ('contact', 'agent'):
2796 if old_text == new_text:
2797 return
2798 for contact in gajim.contacts.get_contacts(account, jid):
2799 contact.name = new_text
2800 gajim.connections[account].update_contact(jid, new_text, \
2801 contact.groups)
2802 self.draw_contact(jid, account)
2803 # Update opened chats
2804 for ctrl in gajim.interface.msg_win_mgr.get_controls(jid, account):
2805 ctrl.update_ui()
2806 win = gajim.interface.msg_win_mgr.get_window(jid, account)
2807 win.redraw_tab(ctrl)
2808 win.show_title()
2809 elif row_type == 'group':
2810 # in C_JID column, we hold the group name (which is not escaped)
2811 self.rename_group(old_text, new_text, account)
2813 def on_canceled():
2814 if 'rename' in gajim.interface.instances:
2815 del gajim.interface.instances['rename']
2817 gajim.interface.instances['rename'] = dialogs.InputDialog(title, message,
2818 old_text, False, (on_renamed, account, row_type, jid, old_text),
2819 on_canceled)
2821 def on_remove_group_item_activated(self, widget, group, account):
2822 def on_ok(checked):
2823 for contact in gajim.contacts.get_contacts_from_group(account, group):
2824 if not checked:
2825 self.remove_contact_from_groups(contact.jid,account, [group])
2826 else:
2827 gajim.connections[account].unsubscribe(contact.jid)
2828 self.remove_contact(contact.jid, account, backend=True)
2830 dialogs.ConfirmationDialogCheck(_('Remove Group'),
2831 _('Do you want to remove group %s from the roster?') % group,
2832 _('Also remove all contacts in this group from your roster'),
2833 on_response_ok=on_ok)
2835 def on_assign_pgp_key(self, widget, contact, account):
2836 attached_keys = gajim.config.get_per('accounts', account,
2837 'attached_gpg_keys').split()
2838 keys = {}
2839 keyID = _('None')
2840 for i in xrange(len(attached_keys)/2):
2841 keys[attached_keys[2*i]] = attached_keys[2*i+1]
2842 if attached_keys[2*i] == contact.jid:
2843 keyID = attached_keys[2*i+1]
2844 public_keys = gajim.connections[account].ask_gpg_keys()
2845 public_keys[_('None')] = _('None')
2847 def on_key_selected(keyID):
2848 if keyID is None:
2849 return
2850 if keyID[0] == _('None'):
2851 if contact.jid in keys:
2852 del keys[contact.jid]
2853 keyID = ''
2854 else:
2855 keyID = keyID[0]
2856 keys[contact.jid] = keyID
2858 ctrl = gajim.interface.msg_win_mgr.get_control(contact.jid, account)
2859 if ctrl:
2860 ctrl.update_ui()
2862 keys_str = ''
2863 for jid in keys:
2864 keys_str += jid + ' ' + keys[jid] + ' '
2865 gajim.config.set_per('accounts', account, 'attached_gpg_keys',
2866 keys_str)
2867 for u in gajim.contacts.get_contacts(account, contact.jid):
2868 u.keyID = helpers.prepare_and_validate_gpg_keyID(account,
2869 contact.jid, keyID)
2871 dialogs.ChooseGPGKeyDialog(_('Assign OpenPGP Key'),
2872 _('Select a key to apply to the contact'), public_keys,
2873 on_key_selected, selected=keyID)
2875 def on_set_custom_avatar_activate(self, widget, contact, account):
2876 def on_ok(widget, path_to_file):
2877 filesize = os.path.getsize(path_to_file) # in bytes
2878 invalid_file = False
2879 msg = ''
2880 if os.path.isfile(path_to_file):
2881 stat = os.stat(path_to_file)
2882 if stat[6] == 0:
2883 invalid_file = True
2884 msg = _('File is empty')
2885 else:
2886 invalid_file = True
2887 msg = _('File does not exist')
2888 if invalid_file:
2889 dialogs.ErrorDialog(_('Could not load image'), msg)
2890 return
2891 try:
2892 pixbuf = gtk.gdk.pixbuf_new_from_file(path_to_file)
2893 if filesize > 16384: # 16 kb
2894 # get the image at 'tooltip size'
2895 # and hope that user did not specify in ACE crazy size
2896 pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'tooltip')
2897 except gobject.GError, msg: # unknown format
2898 # msg should be string, not object instance
2899 msg = str(msg)
2900 dialogs.ErrorDialog(_('Could not load image'), msg)
2901 return
2902 gajim.interface.save_avatar_files(contact.jid, pixbuf, local=True)
2903 dlg.destroy()
2904 self.update_avatar_in_gui(contact.jid, account)
2906 def on_clear(widget):
2907 dlg.destroy()
2908 # Delete file:
2909 gajim.interface.remove_avatar_files(contact.jid, local=True)
2910 self.update_avatar_in_gui(contact.jid, account)
2912 dlg = dialogs.AvatarChooserDialog(on_response_ok=on_ok,
2913 on_response_clear=on_clear)
2915 def on_edit_groups(self, widget, list_):
2916 dialogs.EditGroupsDialog(list_)
2918 def on_history(self, widget, contact, account):
2919 '''When history menuitem is activated: call log window'''
2920 if 'logs' in gajim.interface.instances:
2921 gajim.interface.instances['logs'].window.present()
2922 gajim.interface.instances['logs'].open_history(contact.jid, account)
2923 else:
2924 gajim.interface.instances['logs'] = history_window.\
2925 HistoryWindow(contact.jid, account)
2927 def on_disconnect(self, widget, jid, account):
2928 '''When disconnect menuitem is activated: disconect from room'''
2929 if jid in gajim.interface.minimized_controls[account]:
2930 ctrl = gajim.interface.minimized_controls[account][jid]
2931 ctrl.shutdown()
2932 ctrl.got_disconnected()
2933 self.remove_groupchat(jid, account)
2935 def on_reconnect(self, widget, jid, account):
2936 '''When disconnect menuitem is activated: disconect from room'''
2937 if jid in gajim.interface.minimized_controls[account]:
2938 ctrl = gajim.interface.minimized_controls[account][jid]
2939 gajim.interface.join_gc_room(account, jid, ctrl.nick,
2940 gajim.gc_passwords.get(jid, ''))
2942 def on_send_single_message_menuitem_activate(self, widget, account,
2943 contact=None):
2944 if contact is None:
2945 dialogs.SingleMessageWindow(account, action='send')
2946 elif isinstance(contact, list):
2947 dialogs.SingleMessageWindow(account, contact, 'send')
2948 else:
2949 jid = contact.jid
2950 if contact.jid == gajim.get_jid_from_account(account):
2951 jid += '/' + contact.resource
2952 dialogs.SingleMessageWindow(account, jid, 'send')
2954 def on_send_file_menuitem_activate(self, widget, contact, account,
2955 resource=None):
2956 gajim.interface.instances['file_transfers'].show_file_send_request(
2957 account, contact)
2959 def on_add_special_notification_menuitem_activate(self, widget, jid):
2960 dialogs.AddSpecialNotificationDialog(jid)
2962 def on_invite_to_new_room(self, widget, list_, resource=None):
2963 ''' resource parameter MUST NOT be used if more than one contact in
2964 list '''
2965 account_list = []
2966 jid_list = []
2967 for (contact, account) in list_:
2968 if contact.jid not in jid_list:
2969 if resource: # we MUST have one contact only in list_
2970 fjid = contact.jid + '/' + resource
2971 jid_list.append(fjid)
2972 else:
2973 jid_list.append(contact.jid)
2974 if account not in account_list:
2975 account_list.append(account)
2976 # transform None in 'jabber'
2977 type_ = gajim.get_transport_name_from_jid(jid_list[0]) or 'jabber'
2978 for account in account_list:
2979 if gajim.connections[account].muc_jid[type_]:
2980 # create the room on this muc server
2981 if 'join_gc' in gajim.interface.instances[account]:
2982 gajim.interface.instances[account]['join_gc'].window.destroy()
2983 try:
2984 gajim.interface.instances[account]['join_gc'] = \
2985 dialogs.JoinGroupchatWindow(account,
2986 gajim.connections[account].muc_jid[type_],
2987 automatic = {'invities': jid_list})
2988 except GajimGeneralException:
2989 continue
2990 break
2992 def on_invite_to_room(self, widget, list_, room_jid, room_account,
2993 resource=None):
2994 ''' resource parameter MUST NOT be used if more than one contact in
2995 list '''
2996 for e in list_:
2997 contact = e[0]
2998 contact_jid = contact.jid
2999 if resource: # we MUST have one contact only in list_
3000 contact_jid += '/' + resource
3001 gajim.connections[room_account].send_invite(room_jid, contact_jid)
3003 def on_all_groupchat_maximized(self, widget, group_list):
3004 for (contact, account) in group_list:
3005 self.on_groupchat_maximized(widget, contact.jid, account)
3007 def on_groupchat_maximized(self, widget, jid, account):
3008 '''When a groupchat is maximised'''
3009 if not jid in gajim.interface.minimized_controls[account]:
3010 # Already opened?
3011 gc_control = gajim.interface.msg_win_mgr.get_gc_control(jid, account)
3012 if gc_control:
3013 mw = gajim.interface.msg_win_mgr.get_window(jid, account)
3014 mw.set_active_tab(gc_control)
3015 mw.window.window.focus(gtk.get_current_event_time())
3016 return
3017 ctrl = gajim.interface.minimized_controls[account][jid]
3018 mw = gajim.interface.msg_win_mgr.get_window(jid, account)
3019 if not mw:
3020 mw = gajim.interface.msg_win_mgr.create_window(ctrl.contact,
3021 ctrl.account, ctrl.type_id)
3022 ctrl.parent_win = mw
3023 mw.new_tab(ctrl)
3024 mw.set_active_tab(ctrl)
3025 mw.window.window.focus(gtk.get_current_event_time())
3026 self.remove_groupchat(jid, account)
3028 def on_edit_account(self, widget, account):
3029 if 'accounts' in gajim.interface.instances:
3030 gajim.interface.instances['accounts'].window.present()
3031 else:
3032 gajim.interface.instances['accounts'] = config.AccountsWindow()
3033 gajim.interface.instances['accounts'].select_account(account)
3035 def on_zeroconf_properties(self, widget, account):
3036 if 'accounts' in gajim.interface.instances:
3037 gajim.interface.instances['accounts'].window.present()
3038 else:
3039 gajim.interface.instances['accounts'] = config.AccountsWindow()
3040 gajim.interface.instances['accounts'].select_account(account)
3042 def on_open_gmail_inbox(self, widget, account):
3043 url = gajim.connections[account].gmail_url
3044 if url:
3045 helpers.launch_browser_mailer('url', url)
3047 def on_change_status_message_activate(self, widget, account):
3048 show = gajim.SHOW_LIST[gajim.connections[account].connected]
3049 def on_response(message, pep_dict):
3050 if message is None: # None is if user pressed Cancel
3051 return
3052 self.send_status(account, show, message)
3053 self.send_pep(account, pep_dict)
3054 dialogs.ChangeStatusMessageDialog(on_response, show)
3056 def on_add_to_roster(self, widget, contact, account):
3057 dialogs.AddNewContactWindow(account, contact.jid, contact.name)
3060 def on_roster_treeview_scroll_event(self, widget, event):
3061 self.tooltip.hide_tooltip()
3063 def on_roster_treeview_key_press_event(self, widget, event):
3064 '''when a key is pressed in the treeviews'''
3065 self.tooltip.hide_tooltip()
3066 if event.keyval == gtk.keysyms.Escape:
3067 self.tree.get_selection().unselect_all()
3068 elif event.keyval == gtk.keysyms.F2:
3069 treeselection = self.tree.get_selection()
3070 model, list_of_paths = treeselection.get_selected_rows()
3071 if len(list_of_paths) != 1:
3072 return
3073 path = list_of_paths[0]
3074 type_ = model[path][C_TYPE]
3075 if type_ in ('contact', 'group', 'agent'):
3076 jid = model[path][C_JID].decode('utf-8')
3077 account = model[path][C_ACCOUNT].decode('utf-8')
3078 self.on_rename(widget, type_, jid, account)
3080 elif event.keyval == gtk.keysyms.Delete:
3081 treeselection = self.tree.get_selection()
3082 model, list_of_paths = treeselection.get_selected_rows()
3083 if not len(list_of_paths):
3084 return
3085 type_ = model[list_of_paths[0]][C_TYPE]
3086 account = model[list_of_paths[0]][C_ACCOUNT].decode('utf-8')
3087 if type_ in ('account', 'group', 'self_contact') or \
3088 account == gajim.ZEROCONF_ACC_NAME:
3089 return
3090 list_ = []
3091 for path in list_of_paths:
3092 if model[path][C_TYPE] != type_:
3093 return
3094 jid = model[path][C_JID].decode('utf-8')
3095 account = model[path][C_ACCOUNT].decode('utf-8')
3096 contact = gajim.contacts.get_contact_with_highest_priority(account,
3097 jid)
3098 list_.append((contact, account))
3099 if type_ == 'contact':
3100 self.on_req_usub(widget, list_)
3101 elif type_ == 'agent':
3102 self.on_remove_agent(widget, list_)
3104 def on_roster_treeview_button_release_event(self, widget, event):
3105 try:
3106 path = self.tree.get_path_at_pos(int(event.x), int(event.y))[0]
3107 except TypeError:
3108 return False
3110 if event.button == 1: # Left click
3111 if gajim.single_click and not event.state & gtk.gdk.SHIFT_MASK and \
3112 not event.state & gtk.gdk.CONTROL_MASK:
3113 # Check if button has been pressed on the same row
3114 if self.clicked_path == path:
3115 self.on_row_activated(widget, path)
3116 self.clicked_path = None
3118 def on_roster_treeview_button_press_event(self, widget, event):
3119 # hide tooltip, no matter the button is pressed
3120 self.tooltip.hide_tooltip()
3121 try:
3122 pos = self.tree.get_path_at_pos(int(event.x), int(event.y))
3123 path, x = pos[0], pos[2]
3124 except TypeError:
3125 self.tree.get_selection().unselect_all()
3126 return False
3128 if event.button == 3: # Right click
3129 try:
3130 model, list_of_paths = self.tree.get_selection().get_selected_rows()
3131 except TypeError:
3132 list_of_paths = []
3133 if path not in list_of_paths:
3134 self.tree.get_selection().unselect_all()
3135 self.tree.get_selection().select_path(path)
3136 return self.show_treeview_menu(event)
3138 elif event.button == 2: # Middle click
3139 try:
3140 model, list_of_paths = self.tree.get_selection().get_selected_rows()
3141 except TypeError:
3142 list_of_paths = []
3143 if list_of_paths != [path]:
3144 self.tree.get_selection().unselect_all()
3145 self.tree.get_selection().select_path(path)
3146 type_ = model[path][C_TYPE]
3147 if type_ in ('agent', 'contact', 'self_contact', 'groupchat'):
3148 self.on_row_activated(widget, path)
3149 elif type_ == 'account':
3150 account = model[path][C_ACCOUNT].decode('utf-8')
3151 if account != 'all':
3152 show = gajim.connections[account].connected
3153 if show > 1: # We are connected
3154 self.on_change_status_message_activate(widget, account)
3155 return True
3156 show = helpers.get_global_show()
3157 if show == 'offline':
3158 return True
3159 def on_response(message, pep_dict):
3160 if message is None:
3161 return True
3162 for acct in gajim.connections:
3163 if not gajim.config.get_per('accounts', acct,
3164 'sync_with_global_status'):
3165 continue
3166 current_show = gajim.SHOW_LIST[gajim.connections[acct].\
3167 connected]
3168 self.send_status(acct, current_show, message)
3169 self.send_pep(acct, pep_dict)
3170 dialogs.ChangeStatusMessageDialog(on_response, show)
3171 return True
3173 elif event.button == 1: # Left click
3174 model = self.modelfilter
3175 type_ = model[path][C_TYPE]
3176 # x_min is the x start position of status icon column
3177 if gajim.config.get('avatar_position_in_roster') == 'left':
3178 x_min = gajim.config.get('roster_avatar_width')
3179 else:
3180 x_min = 0
3181 if gajim.single_click and not event.state & gtk.gdk.SHIFT_MASK and \
3182 not event.state & gtk.gdk.CONTROL_MASK:
3183 # Don't handle double click if we press icon of a metacontact
3184 titer = model.get_iter(path)
3185 if x > x_min and x < x_min + 27 and type_ == 'contact' and \
3186 model.iter_has_child(titer):
3187 if (self.tree.row_expanded(path)):
3188 self.tree.collapse_row(path)
3189 else:
3190 self.tree.expand_row(path, False)
3191 return
3192 # We just save on which row we press button, and open chat window on
3193 # button release to be able to do DND without opening chat window
3194 self.clicked_path = path
3195 return
3196 else:
3197 if type_ == 'group' and x < 27:
3198 # first cell in 1st column (the arrow SINGLE clicked)
3199 if (self.tree.row_expanded(path)):
3200 self.tree.collapse_row(path)
3201 else:
3202 self.tree.expand_row(path, False)
3204 elif type_ == 'contact' and x > x_min and x < x_min + 27:
3205 if (self.tree.row_expanded(path)):
3206 self.tree.collapse_row(path)
3207 else:
3208 self.tree.expand_row(path, False)
3210 def on_req_usub(self, widget, list_):
3211 '''Remove a contact. list_ is a list of (contact, account) tuples'''
3212 def on_ok(is_checked, list_):
3213 remove_auth = True
3214 if len(list_) == 1:
3215 contact = list_[0][0]
3216 if contact.sub != 'to' and is_checked:
3217 remove_auth = False
3218 for (contact, account) in list_:
3219 if _('Not in Roster') not in contact.get_shown_groups():
3220 gajim.connections[account].unsubscribe(contact.jid, remove_auth)
3221 self.remove_contact(contact.jid, account, backend=True)
3222 if not remove_auth and contact.sub == 'both':
3223 contact.name = ''
3224 contact.groups = []
3225 contact.sub = 'from'
3226 # we can't see him, but have to set it manually in contact
3227 contact.show = 'offline'
3228 gajim.contacts.add_contact(account, contact)
3229 self.add_contact(contact.jid, account)
3230 def on_ok2(list_):
3231 on_ok(False, list_)
3233 if len(list_) == 1:
3234 contact = list_[0][0]
3235 pritext = _('Contact "%s" will be removed from your roster') % \
3236 contact.get_shown_name()
3237 sectext = _('You are about to remove "%(name)s" (%(jid)s) from your '
3238 'roster.\n') % {'name': contact.get_shown_name(),
3239 'jid': contact.jid}
3240 if contact.sub == 'to':
3241 dialogs.ConfirmationDialog(pritext, sectext + \
3242 _('By removing this contact you also remove authorization '
3243 'resulting in him or her always seeing you as offline.'),
3244 on_response_ok = (on_ok2, list_))
3245 elif _('Not in Roster') in contact.get_shown_groups():
3246 # Contact is not in roster
3247 dialogs.ConfirmationDialog(pritext, sectext + \
3248 _('Do you want to continue?'), on_response_ok = (on_ok2, list_))
3249 else:
3250 dialogs.ConfirmationDialogCheck(pritext, sectext + \
3251 _('By removing this contact you also by default remove '
3252 'authorization resulting in him or her always seeing you as '
3253 'offline.'),
3254 _('I want this contact to know my status after removal'),
3255 on_response_ok = (on_ok, list_))
3256 else:
3257 # several contact to remove at the same time
3258 pritext = _('Contacts will be removed from your roster')
3259 jids = ''
3260 for (contact, account) in list_:
3261 jids += '\n ' + contact.get_shown_name() + ' (%s)' % contact.jid +\
3263 sectext = _('By removing these contacts:%s\nyou also remove '
3264 'authorization resulting in them always seeing you as offline.') % \
3265 jids
3266 dialogs.ConfirmationDialog(pritext, sectext,
3267 on_response_ok = (on_ok2, list_))
3269 def on_send_custom_status(self, widget, contact_list, show, group=None):
3270 '''send custom status'''
3271 # contact_list has only one element except if group != None
3272 def on_response(message, pep_dict):
3273 if message is None: # None if user pressed Cancel
3274 return
3275 account_list = []
3276 for (contact, account) in contact_list:
3277 if account not in account_list:
3278 account_list.append(account)
3279 # 1. update status_sent_to_[groups|users] list
3280 if group:
3281 for account in account_list:
3282 if account not in gajim.interface.status_sent_to_groups:
3283 gajim.interface.status_sent_to_groups[account] = {}
3284 gajim.interface.status_sent_to_groups[account][group] = show
3285 else:
3286 for (contact, account) in contact_list:
3287 if account not in gajim.interface.status_sent_to_users:
3288 gajim.interface.status_sent_to_users[account] = {}
3289 gajim.interface.status_sent_to_users[account][contact.jid] = show
3291 # 2. update privacy lists if main status is invisible
3292 for account in account_list:
3293 if gajim.SHOW_LIST[gajim.connections[account].connected] == \
3294 'invisible':
3295 gajim.connections[account].set_invisible_rule()
3297 # 3. send directed presence
3298 for (contact, account) in contact_list:
3299 our_jid = gajim.get_jid_from_account(account)
3300 jid = contact.jid
3301 if jid == our_jid:
3302 jid += '/' + contact.resource
3303 self.send_status(account, show, message, to=jid)
3305 def send_it(is_checked=None):
3306 if is_checked is not None: # dialog has been shown
3307 if is_checked: # user does not want to be asked again
3308 gajim.config.set('confirm_custom_status', 'no')
3309 else:
3310 gajim.config.set('confirm_custom_status', 'yes')
3311 self.get_status_message(show, on_response, show_pep=False,
3312 always_ask=True)
3314 confirm_custom_status = gajim.config.get('confirm_custom_status')
3315 if confirm_custom_status == 'no':
3316 send_it()
3317 return
3318 pritext = _('You are about to send a custom status. Are you sure you want'
3319 ' to continue?')
3320 sectext = _('This contact will temporarily see you as %(status)s, '
3321 'but only until you change your status. Then he will see your global '
3322 'status.') % {'status': show}
3323 dlg = dialogs.ConfirmationDialogCheck(pritext, sectext,
3324 _('Do _not ask me again'), on_response_ok=send_it)
3326 def on_status_combobox_changed(self, widget):
3327 '''When we change our status via the combobox'''
3328 model = self.status_combobox.get_model()
3329 active = self.status_combobox.get_active()
3330 if active == -1: # no active item
3331 return
3332 if not self.combobox_callback_active:
3333 self.previous_status_combobox_active = active
3334 return
3335 accounts = gajim.connections.keys()
3336 if len(accounts) == 0:
3337 dialogs.ErrorDialog(_('No account available'),
3338 _('You must create an account before you can chat with other contacts.'))
3339 self.update_status_combobox()
3340 return
3341 status = model[active][2].decode('utf-8')
3342 statuses_unified = helpers.statuses_unified() # status "desync'ed" or not
3343 if (active == 7 and statuses_unified) or (active == 9 and \
3344 not statuses_unified):
3345 # 'Change status message' selected:
3346 # do not change show, just show change status dialog
3347 status = model[self.previous_status_combobox_active][2].decode('utf-8')
3348 def on_response(message, pep_dict):
3349 if message is not None: # None if user pressed Cancel
3350 for account in accounts:
3351 if not gajim.config.get_per('accounts', account,
3352 'sync_with_global_status'):
3353 continue
3354 current_show = gajim.SHOW_LIST[
3355 gajim.connections[account].connected]
3356 self.send_status(account, current_show, message)
3357 self.send_pep(account, pep_dict)
3358 self.combobox_callback_active = False
3359 self.status_combobox.set_active(
3360 self.previous_status_combobox_active)
3361 self.combobox_callback_active = True
3362 dialogs.ChangeStatusMessageDialog(on_response, status)
3363 return
3364 # we are about to change show, so save this new show so in case
3365 # after user chooses "Change status message" menuitem
3366 # we can return to this show
3367 self.previous_status_combobox_active = active
3368 connected_accounts = gajim.get_number_of_connected_accounts()
3370 def on_continue(message, pep_dict):
3371 if message is None:
3372 # user pressed Cancel to change status message dialog
3373 self.update_status_combobox()
3374 return
3375 global_sync_accounts = []
3376 for acct in accounts:
3377 if gajim.config.get_per('accounts', acct,
3378 'sync_with_global_status'):
3379 global_sync_accounts.append(acct)
3380 global_sync_connected_accounts = \
3381 gajim.get_number_of_connected_accounts(global_sync_accounts)
3382 for account in accounts:
3383 if not gajim.config.get_per('accounts', account,
3384 'sync_with_global_status'):
3385 continue
3386 # we are connected (so we wanna change show and status)
3387 # or no account is connected and we want to connect with new show
3388 # and status
3390 if not global_sync_connected_accounts > 0 or \
3391 gajim.connections[account].connected > 0:
3392 self.send_status(account, status, message)
3393 self.send_pep(account, pep_dict)
3394 self.update_status_combobox()
3396 if status == 'invisible':
3397 bug_user = False
3398 for account in accounts:
3399 if connected_accounts < 1 or gajim.account_is_connected(account):
3400 if not gajim.config.get_per('accounts', account,
3401 'sync_with_global_status'):
3402 continue
3403 # We're going to change our status to invisible
3404 if self.connected_rooms(account):
3405 bug_user = True
3406 break
3407 if bug_user:
3408 def on_ok():
3409 self.get_status_message(status, on_continue, show_pep=False)
3411 def on_cancel():
3412 self.update_status_combobox()
3414 dialogs.ConfirmationDialog(
3415 _('You are participating in one or more group chats'),
3416 _('Changing your status to invisible will result in '
3417 'disconnection from those group chats. Are you sure you want to '
3418 'go invisible?'), on_reponse_ok=on_ok,
3419 on_response_cancel=on_cancel)
3420 return
3422 self.get_status_message(status, on_continue)
3424 def on_preferences_menuitem_activate(self, widget):
3425 if 'preferences' in gajim.interface.instances:
3426 gajim.interface.instances['preferences'].window.present()
3427 else:
3428 gajim.interface.instances['preferences'] = config.PreferencesWindow()
3430 def on_publish_tune_toggled(self, widget, account):
3431 act = widget.get_active()
3432 gajim.config.set_per('accounts', account, 'publish_tune', act)
3433 if act:
3434 gajim.interface.enable_music_listener()
3435 else:
3436 # disable it only if no other account use it
3437 for acct in gajim.connections:
3438 if gajim.config.get_per('accounts', acct, 'publish_tune'):
3439 break
3440 else:
3441 gajim.interface.disable_music_listener()
3443 if gajim.connections[account].pep_supported:
3444 # As many implementations don't support retracting items, we send a
3445 # "Stopped" event first
3446 pep.user_send_tune(account, '')
3447 pep.user_retract_tune(account)
3448 helpers.update_optional_features(account)
3450 def on_pep_services_menuitem_activate(self, widget, account):
3451 if 'pep_services' in gajim.interface.instances[account]:
3452 gajim.interface.instances[account]['pep_services'].window.present()
3453 else:
3454 gajim.interface.instances[account]['pep_services'] = \
3455 config.ManagePEPServicesWindow(account)
3457 def on_add_new_contact(self, widget, account):
3458 dialogs.AddNewContactWindow(account)
3460 def on_join_gc_activate(self, widget, account):
3461 '''when the join gc menuitem is clicked, show the join gc window'''
3462 invisible_show = gajim.SHOW_LIST.index('invisible')
3463 if gajim.connections[account].connected == invisible_show:
3464 dialogs.ErrorDialog(_('You cannot join a group chat while you are '
3465 'invisible'))
3466 return
3467 if 'join_gc' in gajim.interface.instances[account]:
3468 gajim.interface.instances[account]['join_gc'].window.present()
3469 else:
3470 # c http://nkour.blogspot.com/2005/05/pythons-init-return-none-doesnt-return.html
3471 try:
3472 gajim.interface.instances[account]['join_gc'] = \
3473 dialogs.JoinGroupchatWindow(account)
3474 except GajimGeneralException:
3475 pass
3477 def on_new_chat_menuitem_activate(self, widget, account):
3478 dialogs.NewChatDialog(account)
3480 def on_contents_menuitem_activate(self, widget):
3481 helpers.launch_browser_mailer('url', 'http://trac.gajim.org/wiki')
3483 def on_faq_menuitem_activate(self, widget):
3484 helpers.launch_browser_mailer('url',
3485 'http://trac.gajim.org/wiki/GajimFaq')
3487 def on_features_menuitem_activate(self, widget):
3488 features_window.FeaturesWindow()
3490 def on_about_menuitem_activate(self, widget):
3491 dialogs.AboutDialog()
3493 def on_accounts_menuitem_activate(self, widget):
3494 if 'accounts' in gajim.interface.instances:
3495 gajim.interface.instances['accounts'].window.present()
3496 else:
3497 gajim.interface.instances['accounts'] = config.AccountsWindow()
3499 def on_file_transfers_menuitem_activate(self, widget):
3500 if gajim.interface.instances['file_transfers'].window.get_property(
3501 'visible'):
3502 gajim.interface.instances['file_transfers'].window.present()
3503 else:
3504 gajim.interface.instances['file_transfers'].window.show_all()
3506 def on_history_menuitem_activate(self, widget):
3507 if 'logs' in gajim.interface.instances:
3508 gajim.interface.instances['logs'].window.present()
3509 else:
3510 gajim.interface.instances['logs'] = history_window.\
3511 HistoryWindow()
3513 def on_show_transports_menuitem_activate(self, widget):
3514 gajim.config.set('show_transports_group', widget.get_active())
3515 self.refilter_shown_roster_items()
3517 def on_manage_bookmarks_menuitem_activate(self, widget):
3518 config.ManageBookmarksWindow()
3520 def on_profile_avatar_menuitem_activate(self, widget, account):
3521 gajim.interface.edit_own_details(account)
3523 def on_execute_command(self, widget, contact, account, resource=None):
3524 '''Execute command. Full JID needed; if it is other contact,
3525 resource is necessary. Widget is unnecessary, only to be
3526 able to make this a callback.'''
3527 jid = contact.jid
3528 if resource is not None:
3529 jid = jid + u'/' + resource
3530 adhoc_commands.CommandWindow(account, jid)
3532 def on_roster_window_focus_in_event(self, widget, event):
3533 # roster received focus, so if we had urgency REMOVE IT
3534 # NOTE: we do not have to read the message to remove urgency
3535 # so this functions does that
3536 gtkgui_helpers.set_unset_urgency_hint(widget, False)
3538 # if a contact row is selected, update colors (eg. for status msg)
3539 # because gtk engines may differ in bg when window is selected
3540 # or not
3541 if len(self._last_selected_contact):
3542 for (jid, account) in self._last_selected_contact:
3543 self.draw_contact(jid, account, selected=True, focus=True)
3545 def on_roster_window_focus_out_event(self, widget, event):
3546 # if a contact row is selected, update colors (eg. for status msg)
3547 # because gtk engines may differ in bg when window is selected
3548 # or not
3549 if len(self._last_selected_contact):
3550 for (jid, account) in self._last_selected_contact:
3551 self.draw_contact(jid, account, selected=True, focus=False)
3553 def on_roster_window_key_press_event(self, widget, event):
3554 if event.keyval == gtk.keysyms.Escape:
3555 if gajim.interface.msg_win_mgr.mode == \
3556 MessageWindowMgr.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER and \
3557 gajim.interface.msg_win_mgr.one_window_opened():
3558 # let message window close the tab
3559 return
3560 list_of_paths = self.tree.get_selection().get_selected_rows()[1]
3561 if not len(list_of_paths) and gajim.interface.systray_enabled and \
3562 not gajim.config.get('quit_on_roster_x_button'):
3563 self.tooltip.hide_tooltip()
3564 self.window.hide()
3565 elif event.state & gtk.gdk.CONTROL_MASK and event.keyval == gtk.keysyms.i:
3566 treeselection = self.tree.get_selection()
3567 model, list_of_paths = treeselection.get_selected_rows()
3568 for path in list_of_paths:
3569 type_ = model[path][C_TYPE]
3570 if type_ in ('contact', 'agent'):
3571 jid = model[path][C_JID].decode('utf-8')
3572 account = model[path][C_ACCOUNT].decode('utf-8')
3573 contact = gajim.contacts.get_first_contact_from_jid(account, jid)
3574 self.on_info(widget, contact, account)
3575 elif event.state & gtk.gdk.CONTROL_MASK and event.keyval == gtk.keysyms.h:
3576 treeselection = self.tree.get_selection()
3577 model, list_of_paths = treeselection.get_selected_rows()
3578 if len(list_of_paths) != 1:
3579 return
3580 path = list_of_paths[0]
3581 type_ = model[path][C_TYPE]
3582 if type_ in ('contact', 'agent'):
3583 jid = model[path][C_JID].decode('utf-8')
3584 account = model[path][C_ACCOUNT].decode('utf-8')
3585 contact = gajim.contacts.get_first_contact_from_jid(account, jid)
3586 self.on_history(widget, contact, account)
3588 def on_roster_window_popup_menu(self, widget):
3589 event = gtk.gdk.Event(gtk.gdk.KEY_PRESS)
3590 self.show_treeview_menu(event)
3592 def on_row_activated(self, widget, path):
3593 '''When an iter is activated (double-click or single click if gnome is
3594 set this way)'''
3595 model = self.modelfilter
3596 account = model[path][C_ACCOUNT].decode('utf-8')
3597 type_ = model[path][C_TYPE]
3598 if type_ in ('group', 'account'):
3599 if self.tree.row_expanded(path):
3600 self.tree.collapse_row(path)
3601 else:
3602 self.tree.expand_row(path, False)
3603 return
3604 jid = model[path][C_JID].decode('utf-8')
3605 resource = None
3606 contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
3607 titer = model.get_iter(path)
3608 if contact.is_groupchat():
3609 first_ev = gajim.events.get_first_event(account, jid)
3610 if first_ev and self.open_event(account, jid, first_ev):
3611 # We are invited to a GC
3612 # open event cares about connecting to it
3613 self.remove_groupchat(jid, account)
3614 else:
3615 self.on_groupchat_maximized(None, jid, account)
3616 return
3618 # else
3619 first_ev = gajim.events.get_first_event(account, jid)
3620 if not first_ev:
3621 # look in other resources
3622 for c in gajim.contacts.get_contacts(account, jid):
3623 fjid = c.get_full_jid()
3624 first_ev = gajim.events.get_first_event(account, fjid)
3625 if first_ev:
3626 resource = c.resource
3627 break
3628 if not first_ev and model.iter_has_child(titer):
3629 child_iter = model.iter_children(titer)
3630 while not first_ev and child_iter:
3631 child_jid = model[child_iter][C_JID].decode('utf-8')
3632 first_ev = gajim.events.get_first_event(account, child_jid)
3633 if first_ev:
3634 jid = child_jid
3635 else:
3636 child_iter = model.iter_next(child_iter)
3637 session = None
3638 if first_ev:
3639 if first_ev.type_ in ('chat', 'normal'):
3640 session = first_ev.parameters[8]
3641 fjid = jid
3642 if resource:
3643 fjid += '/' + resource
3644 if self.open_event(account, fjid, first_ev):
3645 return
3646 # else
3647 contact = gajim.contacts.get_contact(account, jid, resource)
3648 if not contact or isinstance(contact, list):
3649 contact = gajim.contacts.get_contact_with_highest_priority(account,
3650 jid)
3651 if jid == gajim.get_jid_from_account(account):
3652 resource = contact.resource
3654 gajim.interface.on_open_chat_window(None, contact, account, \
3655 resource=resource, session=session)
3657 def on_roster_treeview_row_activated(self, widget, path, col=0):
3658 '''When an iter is double clicked: open the first event window'''
3659 if not gajim.single_click:
3660 self.on_row_activated(widget, path)
3662 def on_roster_treeview_row_expanded(self, widget, titer, path):
3663 '''When a row is expanded change the icon of the arrow'''
3664 self._toggeling_row = True
3665 model = widget.get_model()
3666 child_model = model.get_model()
3667 child_iter = model.convert_iter_to_child_iter(titer)
3669 if self.regroup: # merged accounts
3670 accounts = gajim.connections.keys()
3671 else:
3672 accounts = [model[titer][C_ACCOUNT].decode('utf-8')]
3674 type_ = model[titer][C_TYPE]
3675 if type_ == 'group':
3676 group = model[titer][C_JID].decode('utf-8')
3677 child_model[child_iter][C_IMG] = gajim.interface.jabber_state_images[
3678 '16']['opened']
3679 for account in accounts:
3680 if group in gajim.groups[account]: # This account has this group
3681 gajim.groups[account][group]['expand'] = True
3682 if account + group in self.collapsed_rows:
3683 self.collapsed_rows.remove(account + group)
3684 for contact in gajim.contacts.iter_contacts(account):
3685 jid = contact.jid
3686 if group in contact.groups and gajim.contacts.is_big_brother(
3687 account, jid, accounts) and account + group + jid \
3688 not in self.collapsed_rows:
3689 titers = self._get_contact_iter(jid, account)
3690 for titer in titers:
3691 path = model.get_path(titer)
3692 self.tree.expand_row(path, False)
3693 elif type_ == 'account':
3694 account = accounts[0] # There is only one cause we don't use merge
3695 if account in self.collapsed_rows:
3696 self.collapsed_rows.remove(account)
3697 self.draw_account(account)
3698 # When we expand, groups are collapsed. Restore expand state
3699 for group in gajim.groups[account]:
3700 if gajim.groups[account][group]['expand']:
3701 titer = self._get_group_iter(group, account)
3702 if titer:
3703 path = model.get_path(titer)
3704 self.tree.expand_row(path, False)
3705 elif type_ == 'contact':
3706 # Metacontact got toggled, update icon
3707 jid = model[titer][C_JID].decode('utf-8')
3708 account = model[titer][C_ACCOUNT].decode('utf-8')
3709 contact = gajim.contacts.get_contact(account, jid)
3710 for group in contact.groups:
3711 if account + group + jid in self.collapsed_rows:
3712 self.collapsed_rows.remove(account + group + jid)
3713 family = gajim.contacts.get_metacontacts_family(account, jid)
3714 nearby_family = \
3715 self._get_nearby_family_and_big_brother(family, account)[0]
3716 # Redraw all brothers to show pending events
3717 for data in nearby_family:
3718 self.draw_contact(data['jid'], data['account'])
3720 self._toggeling_row = False
3722 def on_roster_treeview_row_collapsed(self, widget, titer, path):
3723 '''When a row is collapsed change the icon of the arrow'''
3724 self._toggeling_row = True
3725 model = widget.get_model()
3726 child_model = model.get_model()
3727 child_iter = model.convert_iter_to_child_iter(titer)
3729 if self.regroup: # merged accounts
3730 accounts = gajim.connections.keys()
3731 else:
3732 accounts = [model[titer][C_ACCOUNT].decode('utf-8')]
3734 type_ = model[titer][C_TYPE]
3735 if type_ == 'group':
3736 child_model[child_iter][C_IMG] = gajim.interface.jabber_state_images[
3737 '16']['closed']
3738 group = model[titer][C_JID].decode('utf-8')
3739 for account in accounts:
3740 if group in gajim.groups[account]: # This account has this group
3741 gajim.groups[account][group]['expand'] = False
3742 if account + group not in self.collapsed_rows:
3743 self.collapsed_rows.append(account + group)
3744 elif type_ == 'account':
3745 account = accounts[0] # There is only one cause we don't use merge
3746 if account not in self.collapsed_rows:
3747 self.collapsed_rows.append(account)
3748 self.draw_account(account)
3749 elif type_ == 'contact':
3750 # Metacontact got toggled, update icon
3751 jid = model[titer][C_JID].decode('utf-8')
3752 account = model[titer][C_ACCOUNT].decode('utf-8')
3753 contact = gajim.contacts.get_contact(account, jid)
3754 for group in contact.groups:
3755 if account + group + jid not in self.collapsed_rows:
3756 self.collapsed_rows.append(account + group + jid)
3757 family = gajim.contacts.get_metacontacts_family(account, jid)
3758 nearby_family = \
3759 self._get_nearby_family_and_big_brother(family, account)[0]
3760 # Redraw all brothers to show pending events
3761 for data in nearby_family:
3762 self.draw_contact(data['jid'], data['account'])
3764 self._toggeling_row = False
3766 def on_modelfilter_row_has_child_toggled(self, model, path, titer):
3767 '''Called when a row has gotten the first or lost its last child row.
3769 Expand Parent if necessary.
3771 if self._toggeling_row:
3772 # Signal is emitted when we write to our model
3773 return
3775 type_ = model[titer][C_TYPE]
3776 account = model[titer][C_ACCOUNT]
3777 if not account:
3778 return
3780 account = account.decode('utf-8')
3782 if type_ == 'contact':
3783 child_iter = model.convert_iter_to_child_iter(titer)
3784 if self.model.iter_has_child(child_iter):
3785 # we are a bigbrother metacontact
3786 # redraw us to show/hide expand icon
3787 if self.filtering:
3788 # Prevent endless loops
3789 jid = model[titer][C_JID].decode('utf-8')
3790 gobject.idle_add(self.draw_contact, jid, account)
3791 elif type_ == 'group':
3792 group = model[titer][C_JID].decode('utf-8')
3793 self._adjust_group_expand_collapse_state(group, account)
3794 elif type_ == 'account':
3795 self._adjust_account_expand_collapse_state(account)
3797 # Selection can change when the model is filtered
3798 # Only write to the model when filtering is finished!
3800 # FIXME: When we are filtering our custom colors are somehow lost
3802 # def on_treeview_selection_changed(self, selection):
3803 # '''Called when selection in TreeView has changed.
3805 # Redraw unselected rows to make status message readable
3806 # on all possible backgrounds.
3807 # '''
3808 # model, list_of_paths = selection.get_selected_rows()
3809 # if len(self._last_selected_contact):
3810 # # update unselected rows
3811 # for (jid, account) in self._last_selected_contact:
3812 # gobject.idle_add(self.draw_contact, jid, account)
3813 # self._last_selected_contact = []
3814 # if len(list_of_paths) == 0:
3815 # return
3816 # for path in list_of_paths:
3817 # row = model[path]
3818 # if row[C_TYPE] != 'contact':
3819 # self._last_selected_contact = []
3820 # return
3821 # jid = row[C_JID].decode('utf-8')
3822 # account = row[C_ACCOUNT].decode('utf-8')
3823 # self._last_selected_contact.append((jid, account))
3824 # gobject.idle_add(self.draw_contact, jid, account, True)
3826 def on_service_disco_menuitem_activate(self, widget, account):
3827 server_jid = gajim.config.get_per('accounts', account, 'hostname')
3828 if server_jid in gajim.interface.instances[account]['disco']:
3829 gajim.interface.instances[account]['disco'][server_jid].\
3830 window.present()
3831 else:
3832 try:
3833 # Object will add itself to the window dict
3834 disco.ServiceDiscoveryWindow(account, address_entry=True)
3835 except GajimGeneralException:
3836 pass
3838 def on_show_offline_contacts_menuitem_activate(self, widget):
3839 '''when show offline option is changed:
3840 redraw the treeview'''
3841 gajim.config.set('showoffline', not gajim.config.get('showoffline'))
3842 self.refilter_shown_roster_items()
3843 w = self.xml.get_widget('show_only_active_contacts_menuitem')
3844 if gajim.config.get('showoffline'):
3845 # We need to filter twice to show groups with no contacts inside
3846 # in the correct expand state
3847 self.refilter_shown_roster_items()
3848 w.set_sensitive(False)
3849 else:
3850 w.set_sensitive(True)
3852 def on_show_only_active_contacts_menuitem_activate(self, widget):
3853 '''when show only active contact option is changed:
3854 redraw the treeview'''
3855 gajim.config.set('show_only_chat_and_online', not gajim.config.get(
3856 'show_only_chat_and_online'))
3857 self.refilter_shown_roster_items()
3858 w = self.xml.get_widget('show_offline_contacts_menuitem')
3859 if gajim.config.get('show_only_chat_and_online'):
3860 # We need to filter twice to show groups with no contacts inside
3861 # in the correct expand state
3862 self.refilter_shown_roster_items()
3863 w.set_sensitive(False)
3864 else:
3865 w.set_sensitive(True)
3867 def on_view_menu_activate(self, widget):
3868 # Hide the show roster menu if we are not in the right windowing mode.
3869 if self.hpaned.get_child2() is not None:
3870 self.xml.get_widget('show_roster_menuitem').show()
3871 else:
3872 self.xml.get_widget('show_roster_menuitem').hide()
3874 def on_show_roster_menuitem_toggled(self, widget):
3875 # when num controls is 0 this menuitem is hidden, but still need to
3876 # disable keybinding
3877 if self.hpaned.get_child2() is not None:
3878 self.show_roster_vbox(widget.get_active())
3880 ################################################################################
3881 ### Drag and Drop handling
3882 ################################################################################
3884 def drag_data_get_data(self, treeview, context, selection, target_id, etime):
3885 model, list_of_paths = self.tree.get_selection().get_selected_rows()
3886 if len(list_of_paths) != 1:
3887 return
3888 path = list_of_paths[0]
3889 data = ''
3890 if len(path) >= 3:
3891 data = model[path][C_JID]
3892 selection.set(selection.target, 8, data)
3894 def drag_begin(self, treeview, context):
3895 self.dragging = True
3897 def drag_end(self, treeview, context):
3898 self.dragging = False
3900 def on_drop_rosterx(self, widget, account_source, c_source, account_dest,
3901 c_dest, was_big_brother, context, etime):
3902 gajim.connections[account_dest].send_contacts([c_source], c_dest.jid)
3904 def on_drop_in_contact(self, widget, account_source, c_source, account_dest,
3905 c_dest, was_big_brother, context, etime):
3907 if not gajim.connections[account_source].private_storage_supported or not\
3908 gajim.connections[account_dest].private_storage_supported:
3909 dialogs.WarningDialog(_('Metacontacts storage not supported by your '
3910 'server'),
3911 _('Your server does not support storing metacontacts information. '
3912 'So those information will not be saved on next reconnection.'))
3914 def merge_contacts(is_checked=None):
3915 contacts = 0
3916 if is_checked is not None: # dialog has been shown
3917 if is_checked: # user does not want to be asked again
3918 gajim.config.set('confirm_metacontacts', 'no')
3919 else:
3920 gajim.config.set('confirm_metacontacts', 'yes')
3922 # We might have dropped on a metacontact.
3923 # Remove it and readd later with updated family info
3924 dest_family = gajim.contacts.get_metacontacts_family(account_dest,
3925 c_dest.jid)
3926 if dest_family:
3927 self._remove_metacontact_family(dest_family, account_dest)
3928 source_family = gajim.contacts.get_metacontacts_family(account_source, c_source.jid)
3929 if dest_family == source_family:
3930 n = contacts = len(dest_family)
3931 for tag in source_family:
3932 if tag['jid'] == c_source.jid:
3933 tag['order'] = contacts
3934 continue
3935 if 'order' in tag:
3936 n -= 1
3937 tag['order'] = n
3938 else:
3939 self._remove_entity(c_dest, account_dest)
3941 old_family = gajim.contacts.get_metacontacts_family(account_source,
3942 c_source.jid)
3943 old_groups = c_source.groups
3945 # Remove old source contact(s)
3946 if was_big_brother:
3947 # We have got little brothers. Readd them all
3948 self._remove_metacontact_family(old_family, account_source)
3949 else:
3950 # We are only a litle brother. Simply remove us from our big brother
3951 if self._get_contact_iter(c_source.jid, account_source):
3952 # When we have been in the group before.
3953 # Do not try to remove us again
3954 self._remove_entity(c_source, account_source)
3956 own_data = {}
3957 own_data['jid'] = c_source.jid
3958 own_data['account'] = account_source
3959 # Don't touch the rest of the family
3960 old_family = [own_data]
3962 # Apply new tag and update contact
3963 for data in old_family:
3964 if account_source != data['account'] and not self.regroup:
3965 continue
3967 _account = data['account']
3968 _jid = data['jid']
3969 _contact = gajim.contacts.get_first_contact_from_jid(_account, _jid)
3970 if not _contact:
3971 # One of the metacontacts may be not connected.
3972 continue
3974 _contact.groups = c_dest.groups[:]
3975 gajim.contacts.add_metacontact(account_dest, c_dest.jid,
3976 _account, _contact.jid, contacts)
3977 gajim.connections[account_source].update_contact(_contact.jid,
3978 _contact.name, _contact.groups)
3980 # Re-add all and update GUI
3981 new_family = gajim.contacts.get_metacontacts_family(account_source,
3982 c_source.jid)
3983 brothers = self._add_metacontact_family(new_family, account_source)
3985 for c, acc in brothers:
3986 self.draw_completely(c.jid, acc)
3988 old_groups.extend(c_dest.groups)
3989 for g in old_groups:
3990 self.draw_group(g, account_source)
3992 self.draw_account(account_source)
3993 context.finish(True, True, etime)
3995 confirm_metacontacts = gajim.config.get('confirm_metacontacts')
3996 if confirm_metacontacts == 'no':
3997 merge_contacts()
3998 return
3999 pritext = _('You are about to create a metacontact. Are you sure you want'
4000 ' to continue?')
4001 sectext = _('Metacontacts are a way to regroup several contacts in one '
4002 'line. Generally it is used when the same person has several Jabber '
4003 'accounts or transport accounts.')
4004 dlg = dialogs.ConfirmationDialogCheck(pritext, sectext,
4005 _('Do _not ask me again'), on_response_ok=merge_contacts)
4006 if not confirm_metacontacts: # First time we see this window
4007 dlg.checkbutton.set_active(True)
4010 def on_drop_in_group(self, widget, account, c_source, grp_dest,
4011 is_big_brother, context, etime, grp_source = None):
4012 if is_big_brother:
4013 # add whole metacontact to new group
4014 self.add_contact_to_groups(c_source.jid, account, [grp_dest,])
4015 # remove afterwards so the contact is not moved to General in the
4016 # meantime
4017 if grp_dest != grp_source:
4018 self.remove_contact_from_groups(c_source.jid, account, [grp_source])
4019 else:
4020 # Normal contact or little brother
4021 family = gajim.contacts.get_metacontacts_family(account,
4022 c_source.jid)
4023 if family:
4024 # Little brother
4025 # Remove whole family. Remove us from the family.
4026 # Then re-add other family members.
4027 self._remove_metacontact_family(family, account)
4028 gajim.contacts.remove_metacontact(account, c_source.jid)
4029 for data in family:
4030 if account != data['account'] and not self.regroup:
4031 continue
4032 if data['jid'] == c_source.jid and\
4033 data['account'] == account:
4034 continue
4035 self.add_contact(data['jid'], data['account'])
4036 break
4038 self.add_contact_to_groups(c_source.jid, account, [grp_dest,])
4040 else:
4041 # Normal contact
4042 self.add_contact_to_groups(c_source.jid, account, [grp_dest,])
4043 # remove afterwards so the contact is not moved to General in the
4044 # meantime
4045 if grp_dest != grp_source:
4046 self.remove_contact_from_groups(c_source.jid, account,
4047 [grp_source])
4049 if context.action in (gtk.gdk.ACTION_MOVE, gtk.gdk.ACTION_COPY):
4050 context.finish(True, True, etime)
4053 def drag_drop(self, treeview, context, x, y, timestamp):
4054 target_list = treeview.drag_dest_get_target_list()
4055 target = treeview.drag_dest_find_target(context, target_list)
4056 treeview.drag_get_data(context, target)
4057 context.finish(False, True)
4058 return True
4060 def drag_data_received_data(self, treeview, context, x, y, selection, info,
4061 etime):
4062 treeview.stop_emission('drag_data_received')
4063 drop_info = treeview.get_dest_row_at_pos(x, y)
4064 if not drop_info:
4065 return
4066 if not selection.data:
4067 return # prevents tb when several entrys are dragged
4068 model = treeview.get_model()
4069 data = selection.data
4070 path_dest, position = drop_info
4072 if position == gtk.TREE_VIEW_DROP_BEFORE and len(path_dest) == 2 \
4073 and path_dest[1] == 0: # dropped before the first group
4074 return
4075 if position == gtk.TREE_VIEW_DROP_BEFORE and len(path_dest) == 2:
4076 # dropped before a group: we drop it in the previous group every time
4077 path_dest = (path_dest[0], path_dest[1]-1)
4078 # destination: the row something got dropped on
4079 iter_dest = model.get_iter(path_dest)
4080 type_dest = model[iter_dest][C_TYPE].decode('utf-8')
4081 jid_dest = model[iter_dest][C_JID].decode('utf-8')
4082 account_dest = model[iter_dest][C_ACCOUNT].decode('utf-8')
4084 # drop on account row in merged mode, we cannot know the desired account
4085 if account_dest == 'all':
4086 return
4087 # nothing can be done, if destination account is offline
4088 if gajim.connections[account_dest].connected < 2:
4089 return
4091 # A file got dropped on the roster
4092 if info == self.TARGET_TYPE_URI_LIST:
4093 if len(path_dest) < 3:
4094 return
4095 if type_dest != 'contact':
4096 return
4097 c_dest = gajim.contacts.get_contact_with_highest_priority(account_dest,
4098 jid_dest)
4099 if not gajim.capscache.is_supported(c_dest, NS_FILE):
4100 return
4101 uri = data.strip()
4102 uri_splitted = uri.split() # we may have more than one file dropped
4103 try:
4104 # This is always the last element in windows
4105 uri_splitted.remove('\0')
4106 except ValueError:
4107 pass
4108 nb_uri = len(uri_splitted)
4109 # Check the URIs
4110 bad_uris = []
4111 for a_uri in uri_splitted:
4112 path = helpers.get_file_path_from_dnd_dropped_uri(a_uri)
4113 if not os.path.isfile(path):
4114 bad_uris.append(a_uri)
4115 if len(bad_uris):
4116 dialogs.ErrorDialog(_('Invalid file URI:'), '\n'.join(bad_uris))
4117 return
4118 def _on_send_files(account, jid, uris):
4119 c = gajim.contacts.get_contact_with_highest_priority(account, jid)
4120 for uri in uris:
4121 path = helpers.get_file_path_from_dnd_dropped_uri(uri)
4122 if os.path.isfile(path): # is it file?
4123 gajim.interface.instances['file_transfers'].send_file(
4124 account, c, path)
4125 # Popup dialog to confirm sending
4126 prim_text = 'Send file?'
4127 sec_text = i18n.ngettext('Do you want to send this file to %s:',
4128 'Do you want to send these files to %s:', nb_uri) %\
4129 c_dest.get_shown_name()
4130 for uri in uri_splitted:
4131 path = helpers.get_file_path_from_dnd_dropped_uri(uri)
4132 sec_text += '\n' + os.path.basename(path)
4133 dialog = dialogs.NonModalConfirmationDialog(prim_text, sec_text,
4134 on_response_ok = (_on_send_files, account_dest, jid_dest,
4135 uri_splitted))
4136 dialog.popup()
4137 return
4139 # a roster entry was dragged and dropped somewhere in the roster
4141 # source: the row that was dragged
4142 path_source = treeview.get_selection().get_selected_rows()[1][0]
4143 iter_source = model.get_iter(path_source)
4144 type_source = model[iter_source][C_TYPE]
4145 account_source = model[iter_source][C_ACCOUNT].decode('utf-8')
4147 # Only normal contacts can be dragged
4148 if type_source != 'contact':
4149 return
4150 if gajim.config.get_per('accounts', account_source, 'is_zeroconf'):
4151 return
4153 # A contact was dropped
4154 if gajim.config.get_per('accounts', account_dest, 'is_zeroconf'):
4155 # drop on zeroconf account, adding not possible
4156 return
4157 if type_dest == 'self_contact':
4158 # drop on self contact row
4159 return
4160 if type_dest == 'account' and account_source == account_dest:
4161 # drop on the account it was dragged from
4162 return
4163 if type_dest == 'groupchat':
4164 # drop on a minimized groupchat
4165 # TODO: Invite to groupchat
4166 return
4168 # Get valid source group, jid and contact
4169 it = iter_source
4170 while model[it][C_TYPE] == 'contact':
4171 it = model.iter_parent(it)
4172 grp_source = model[it][C_JID].decode('utf-8')
4173 if grp_source in helpers.special_groups and \
4174 grp_source not in ('Not in Roster', 'Observers'):
4175 # a transport or a minimized groupchat was dragged
4176 # we can add it to other accounts but not move it to another group,
4177 # see below
4178 return
4179 jid_source = data.decode('utf-8')
4180 c_source = gajim.contacts.get_contact_with_highest_priority(
4181 account_source, jid_source)
4183 # Get destination group
4184 grp_dest = None
4185 if type_dest == 'group':
4186 grp_dest = model[iter_dest][C_JID].decode('utf-8')
4187 elif type_dest in ('contact', 'agent'):
4188 it = iter_dest
4189 while model[it][C_TYPE] != 'group':
4190 it = model.iter_parent(it)
4191 grp_dest = model[it][C_JID].decode('utf-8')
4192 if grp_dest in helpers.special_groups:
4193 return
4195 if jid_source == jid_dest:
4196 if grp_source == grp_dest and account_source == account_dest:
4197 # Drop on self
4198 return
4200 # contact drop somewhere in or on a foreign account
4201 if (type_dest == 'account' or not self.regroup) and \
4202 account_source != account_dest:
4203 # add to account in specified group
4204 dialogs.AddNewContactWindow(account=account_dest, jid=jid_source,
4205 user_nick=c_source.name, group=grp_dest)
4206 return
4208 # we may not add contacts from special_groups
4209 if grp_source in helpers.special_groups :
4210 return
4212 # Is the contact we drag a meta contact?
4213 accounts = (self.regroup and gajim.contacts.get_accounts()) or \
4214 account_source
4215 is_big_brother = gajim.contacts.is_big_brother(account_source, jid_source,
4216 accounts)
4218 drop_in_middle_of_meta = False
4219 if type_dest == 'contact':
4220 if position == gtk.TREE_VIEW_DROP_BEFORE and len(path_dest) == 4:
4221 drop_in_middle_of_meta = True
4222 if position == gtk.TREE_VIEW_DROP_AFTER and (len(path_dest) == 4 or \
4223 self.modelfilter.iter_has_child(iter_dest)):
4224 drop_in_middle_of_meta = True
4225 # Contact drop on group row or between two contacts that are
4226 # not metacontacts
4227 if (type_dest == 'group' or position in (gtk.TREE_VIEW_DROP_BEFORE,
4228 gtk.TREE_VIEW_DROP_AFTER)) and not drop_in_middle_of_meta:
4229 self.on_drop_in_group(None, account_source, c_source, grp_dest,
4230 is_big_brother, context, etime, grp_source)
4231 return
4233 # Contact drop on another contact, make meta contacts
4234 if position == gtk.TREE_VIEW_DROP_INTO_OR_AFTER or \
4235 position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE or drop_in_middle_of_meta:
4236 c_dest = gajim.contacts.get_contact_with_highest_priority(account_dest,
4237 jid_dest)
4238 if not c_dest:
4239 # c_dest is None if jid_dest doesn't belong to account
4240 return
4241 menu = gtk.Menu()
4242 item = gtk.MenuItem(_('Send %s to %s') % (c_source.get_shown_name(),
4243 c_dest.get_shown_name()))
4244 item.connect('activate', self.on_drop_rosterx, account_source,
4245 c_source, account_dest, c_dest, is_big_brother, context, etime)
4246 menu.append(item)
4248 item = gtk.MenuItem(_('Make %s and %s metacontacts') % (
4249 c_source.get_shown_name(), c_dest.get_shown_name()))
4250 item.connect('activate', self.on_drop_in_contact, account_source,
4251 c_source, account_dest, c_dest, is_big_brother, context, etime)
4253 menu.append(item)
4255 menu.attach_to_widget(self.tree, None)
4256 menu.connect('selection-done', gtkgui_helpers.destroy_widget)
4257 menu.show_all()
4258 menu.popup(None, None, None, 1, etime)
4260 ################################################################################
4261 ### Everything about images and icons....
4262 ### Cleanup assigned to Jim++ :-)
4263 ################################################################################
4265 def get_appropriate_state_images(self, jid, size='16', icon_name='online'):
4266 '''check jid and return the appropriate state images dict for
4267 the demanded size. icon_name is taken into account when jid is from
4268 transport: transport iconset doesn't contain all icons, so we fall back
4269 to jabber one'''
4270 transport = gajim.get_transport_name_from_jid(jid)
4271 if transport and size in self.transports_state_images:
4272 if transport not in self.transports_state_images[size]:
4273 # we don't have iconset for this transport loaded yet. Let's do it
4274 self.make_transport_state_images(transport)
4275 if transport in self.transports_state_images[size] and \
4276 icon_name in self.transports_state_images[size][transport]:
4277 return self.transports_state_images[size][transport]
4278 return gajim.interface.jabber_state_images[size]
4280 def make_transport_state_images(self, transport):
4281 '''initialise opened and closed 'transport' iconset dict'''
4282 if gajim.config.get('use_transports_iconsets'):
4283 folder = os.path.join(helpers.get_transport_path(transport),
4284 '16x16')
4285 pixo, pixc = gtkgui_helpers.load_icons_meta()
4286 self.transports_state_images['opened'][transport] = \
4287 gtkgui_helpers.load_iconset(folder, pixo, transport=True)
4288 self.transports_state_images['closed'][transport] = \
4289 gtkgui_helpers.load_iconset(folder, pixc, transport=True)
4290 folder = os.path.join(helpers.get_transport_path(transport), '32x32')
4291 self.transports_state_images['32'][transport] = \
4292 gtkgui_helpers.load_iconset(folder, transport=True)
4293 folder = os.path.join(helpers.get_transport_path(transport), '16x16')
4294 self.transports_state_images['16'][transport] = \
4295 gtkgui_helpers.load_iconset(folder, transport=True)
4297 def update_jabber_state_images(self):
4298 # Update the roster
4299 self.setup_and_draw_roster()
4300 # Update the status combobox
4301 model = self.status_combobox.get_model()
4302 titer = model.get_iter_root()
4303 while titer:
4304 if model[titer][2] != '':
4305 # If it's not change status message iter
4306 # eg. if it has show parameter not ''
4307 model[titer][1] = gajim.interface.jabber_state_images['16'][model[
4308 titer][2]]
4309 titer = model.iter_next(titer)
4310 # Update the systray
4311 if gajim.interface.systray_enabled:
4312 gajim.interface.systray.set_img()
4314 for win in gajim.interface.msg_win_mgr.windows():
4315 for ctrl in win.controls():
4316 ctrl.update_ui()
4317 win.redraw_tab(ctrl)
4319 self.update_status_combobox()
4321 def set_account_status_icon(self, account):
4322 status = gajim.connections[account].connected
4323 child_iterA = self._get_account_iter(account, self.model)
4324 if not child_iterA:
4325 return
4326 if not self.regroup:
4327 show = gajim.SHOW_LIST[status]
4328 else: # accounts merged
4329 show = helpers.get_global_show()
4330 self.model[child_iterA][C_IMG] = gajim.interface.jabber_state_images[
4331 '16'][show]
4333 ################################################################################
4334 ### Style and theme related methods
4335 ################################################################################
4337 def show_title(self):
4338 change_title_allowed = gajim.config.get('change_roster_title')
4339 if not change_title_allowed:
4340 return
4342 if gajim.config.get('one_message_window') == 'always_with_roster':
4343 # always_with_roster mode defers to the MessageWindow
4344 if not gajim.interface.msg_win_mgr.one_window_opened():
4345 # No MessageWindow to defer to
4346 self.window.set_title('Gajim')
4347 return
4349 nb_unread = 0
4350 start = ''
4351 for account in gajim.connections:
4352 # Count events in roster title only if we don't auto open them
4353 if not helpers.allow_popup_window(account):
4354 nb_unread += gajim.events.get_nb_events(['chat', 'normal',
4355 'file-request', 'file-error', 'file-completed',
4356 'file-request-error', 'file-send-error', 'file-stopped',
4357 'printed_chat'], account)
4358 if nb_unread > 1:
4359 start = '[' + str(nb_unread) + '] '
4360 elif nb_unread == 1:
4361 start = '* '
4363 self.window.set_title(start + 'Gajim')
4365 gtkgui_helpers.set_unset_urgency_hint(self.window, nb_unread)
4367 def _change_style(self, model, path, titer, option):
4368 if option is None or model[titer][C_TYPE] == option:
4369 # We changed style for this type of row
4370 model[titer][C_NAME] = model[titer][C_NAME]
4372 def change_roster_style(self, option):
4373 self.model.foreach(self._change_style, option)
4374 for win in gajim.interface.msg_win_mgr.windows():
4375 win.repaint_themed_widgets()
4377 def repaint_themed_widgets(self):
4378 '''Notify windows that contain themed widgets to repaint them'''
4379 for win in gajim.interface.msg_win_mgr.windows():
4380 win.repaint_themed_widgets()
4381 for account in gajim.connections:
4382 for addr in gajim.interface.instances[account]['disco']:
4383 gajim.interface.instances[account]['disco'][addr].paint_banner()
4384 for ctrl in gajim.interface.minimized_controls[account].values():
4385 ctrl.repaint_themed_widgets()
4387 def update_avatar_in_gui(self, jid, account):
4388 # Update roster
4389 self.draw_avatar(jid, account)
4390 # Update chat window
4392 ctrl = gajim.interface.msg_win_mgr.get_control(jid, account)
4393 if ctrl:
4394 ctrl.show_avatar()
4396 def on_roster_treeview_style_set(self, treeview, style):
4397 '''When style (theme) changes, redraw all contacts'''
4398 for contact in self._iter_contact_rows():
4399 self.draw_contact(contact[C_JID].decode('utf-8'),
4400 contact[C_ACCOUNT].decode('utf-8'))
4402 def set_renderer_color(self, renderer, style, set_background=True):
4403 '''set style for treeview cell, using PRELIGHT system color'''
4404 if set_background:
4405 bgcolor = self.tree.style.bg[style]
4406 renderer.set_property('cell-background-gdk', bgcolor)
4407 else:
4408 fgcolor = self.tree.style.fg[style]
4409 renderer.set_property('foreground-gdk', fgcolor)
4411 def _iconCellDataFunc(self, column, renderer, model, titer, data=None):
4412 '''When a row is added, set properties for icon renderer'''
4413 theme = gajim.config.get('roster_theme')
4414 type_ = model[titer][C_TYPE]
4415 if type_ == 'account':
4416 color = gajim.config.get_per('themes', theme, 'accountbgcolor')
4417 if color:
4418 renderer.set_property('cell-background', color)
4419 else:
4420 self.set_renderer_color(renderer, gtk.STATE_ACTIVE)
4421 renderer.set_property('xalign', 0)
4422 elif type_ == 'group':
4423 color = gajim.config.get_per('themes', theme, 'groupbgcolor')
4424 if color:
4425 renderer.set_property('cell-background', color)
4426 else:
4427 self.set_renderer_color(renderer, gtk.STATE_PRELIGHT)
4428 renderer.set_property('xalign', 0.2)
4429 elif type_: # prevent type_ = None, see http://trac.gajim.org/ticket/2534
4430 if not model[titer][C_JID] or not model[titer][C_ACCOUNT]:
4431 # This can append when at the moment we add the row
4432 return
4433 jid = model[titer][C_JID].decode('utf-8')
4434 account = model[titer][C_ACCOUNT].decode('utf-8')
4435 if jid in gajim.newly_added[account]:
4436 renderer.set_property('cell-background', gajim.config.get(
4437 'just_connected_bg_color'))
4438 elif jid in gajim.to_be_removed[account]:
4439 renderer.set_property('cell-background', gajim.config.get(
4440 'just_disconnected_bg_color'))
4441 else:
4442 color = gajim.config.get_per('themes', theme, 'contactbgcolor')
4443 if color:
4444 renderer.set_property('cell-background', color)
4445 else:
4446 renderer.set_property('cell-background', None)
4447 parent_iter = model.iter_parent(titer)
4448 if model[parent_iter][C_TYPE] == 'contact':
4449 renderer.set_property('xalign', 1)
4450 else:
4451 renderer.set_property('xalign', 0.4)
4452 renderer.set_property('width', 26)
4454 def _nameCellDataFunc(self, column, renderer, model, titer, data=None):
4455 '''When a row is added, set properties for name renderer'''
4456 theme = gajim.config.get('roster_theme')
4457 type_ = model[titer][C_TYPE]
4458 if type_ == 'account':
4459 color = gajim.config.get_per('themes', theme, 'accounttextcolor')
4460 if color:
4461 renderer.set_property('foreground', color)
4462 else:
4463 self.set_renderer_color(renderer, gtk.STATE_ACTIVE, False)
4464 color = gajim.config.get_per('themes', theme, 'accountbgcolor')
4465 if color:
4466 renderer.set_property('cell-background', color)
4467 else:
4468 self.set_renderer_color(renderer, gtk.STATE_ACTIVE)
4469 renderer.set_property('font',
4470 gtkgui_helpers.get_theme_font_for_option(theme, 'accountfont'))
4471 renderer.set_property('xpad', 0)
4472 renderer.set_property('width', 3)
4473 elif type_ == 'group':
4474 color = gajim.config.get_per('themes', theme, 'grouptextcolor')
4475 if color:
4476 renderer.set_property('foreground', color)
4477 else:
4478 self.set_renderer_color(renderer, gtk.STATE_PRELIGHT, False)
4479 color = gajim.config.get_per('themes', theme, 'groupbgcolor')
4480 if color:
4481 renderer.set_property('cell-background', color)
4482 else:
4483 self.set_renderer_color(renderer, gtk.STATE_PRELIGHT)
4484 renderer.set_property('font',
4485 gtkgui_helpers.get_theme_font_for_option(theme, 'groupfont'))
4486 renderer.set_property('xpad', 4)
4487 elif type_: # prevent type_ = None, see http://trac.gajim.org/ticket/2534
4488 if not model[titer][C_JID] or not model[titer][C_ACCOUNT]:
4489 # This can append when at the moment we add the row
4490 return
4491 jid = model[titer][C_JID].decode('utf-8')
4492 account = model[titer][C_ACCOUNT].decode('utf-8')
4493 color = None
4494 if type_ == 'groupchat':
4495 ctrl = gajim.interface.minimized_controls[account].get(jid, None)
4496 if ctrl and ctrl.attention_flag:
4497 color = gajim.config.get_per('themes', theme,
4498 'state_muc_directed_msg_color')
4499 renderer.set_property('foreground', 'red')
4500 if not color:
4501 color = gajim.config.get_per('themes', theme, 'contacttextcolor')
4502 if color:
4503 renderer.set_property('foreground', color)
4504 else:
4505 renderer.set_property('foreground', None)
4506 if jid in gajim.newly_added[account]:
4507 renderer.set_property('cell-background', gajim.config.get(
4508 'just_connected_bg_color'))
4509 elif jid in gajim.to_be_removed[account]:
4510 renderer.set_property('cell-background', gajim.config.get(
4511 'just_disconnected_bg_color'))
4512 else:
4513 color = gajim.config.get_per('themes', theme, 'contactbgcolor')
4514 if color:
4515 renderer.set_property('cell-background', color)
4516 else:
4517 renderer.set_property('cell-background', None)
4518 renderer.set_property('font',
4519 gtkgui_helpers.get_theme_font_for_option(theme, 'contactfont'))
4520 parent_iter = model.iter_parent(titer)
4521 if model[parent_iter][C_TYPE] == 'contact':
4522 renderer.set_property('xpad', 16)
4523 else:
4524 renderer.set_property('xpad', 8)
4527 def _fill_mood_pixbuf_renderer(self, column, renderer, model, titer,
4528 data = None):
4529 '''When a row is added, set properties for avatar renderer'''
4530 theme = gajim.config.get('roster_theme')
4531 type_ = model[titer][C_TYPE]
4532 if type_ == 'group':
4533 renderer.set_property('visible', False)
4534 return
4536 # allocate space for the icon only if needed
4537 if model[titer][C_MOOD_PIXBUF]:
4538 renderer.set_property('visible', True)
4539 else:
4540 renderer.set_property('visible', False)
4541 if type_ == 'account':
4542 color = gajim.config.get_per('themes', theme,
4543 'accountbgcolor')
4544 if color:
4545 renderer.set_property('cell-background', color)
4546 else:
4547 self.set_renderer_color(renderer,
4548 gtk.STATE_ACTIVE)
4549 # align pixbuf to the right)
4550 renderer.set_property('xalign', 1)
4551 # prevent type_ = None, see http://trac.gajim.org/ticket/2534
4552 elif type_:
4553 if not model[titer][C_JID] \
4554 or not model[titer][C_ACCOUNT]:
4555 # This can append at the moment we add the row
4556 return
4557 jid = model[titer][C_JID].decode('utf-8')
4558 account = model[titer][C_ACCOUNT].decode('utf-8')
4559 if jid in gajim.newly_added[account]:
4560 renderer.set_property('cell-background',
4561 gajim.config.get(
4562 'just_connected_bg_color'))
4563 elif jid in gajim.to_be_removed[account]:
4564 renderer.set_property('cell-background',
4565 gajim.config.get(
4566 'just_disconnected_bg_color'))
4567 else:
4568 color = gajim.config.get_per('themes',
4569 theme, 'contactbgcolor')
4570 if color:
4571 renderer.set_property(
4572 'cell-background', color)
4573 else:
4574 renderer.set_property(
4575 'cell-background', None)
4576 # align pixbuf to the right
4577 renderer.set_property('xalign', 1)
4580 def _fill_activity_pixbuf_renderer(self, column, renderer, model, titer,
4581 data = None):
4582 '''When a row is added, set properties for avatar renderer'''
4583 theme = gajim.config.get('roster_theme')
4584 type_ = model[titer][C_TYPE]
4585 if type_ == 'group':
4586 renderer.set_property('visible', False)
4587 return
4589 # allocate space for the icon only if needed
4590 if model[titer][C_ACTIVITY_PIXBUF]:
4591 renderer.set_property('visible', True)
4592 else:
4593 renderer.set_property('visible', False)
4594 if type_ == 'account':
4595 color = gajim.config.get_per('themes', theme,
4596 'accountbgcolor')
4597 if color:
4598 renderer.set_property('cell-background', color)
4599 else:
4600 self.set_renderer_color(renderer,
4601 gtk.STATE_ACTIVE)
4602 # align pixbuf to the right)
4603 renderer.set_property('xalign', 1)
4604 # prevent type_ = None, see http://trac.gajim.org/ticket/2534
4605 elif type_:
4606 if not model[titer][C_JID] \
4607 or not model[titer][C_ACCOUNT]:
4608 # This can append at the moment we add the row
4609 return
4610 jid = model[titer][C_JID].decode('utf-8')
4611 account = model[titer][C_ACCOUNT].decode('utf-8')
4612 if jid in gajim.newly_added[account]:
4613 renderer.set_property('cell-background',
4614 gajim.config.get(
4615 'just_connected_bg_color'))
4616 elif jid in gajim.to_be_removed[account]:
4617 renderer.set_property('cell-background',
4618 gajim.config.get(
4619 'just_disconnected_bg_color'))
4620 else:
4621 color = gajim.config.get_per('themes',
4622 theme, 'contactbgcolor')
4623 if color:
4624 renderer.set_property(
4625 'cell-background', color)
4626 else:
4627 renderer.set_property(
4628 'cell-background', None)
4629 # align pixbuf to the right
4630 renderer.set_property('xalign', 1)
4633 def _fill_tune_pixbuf_renderer(self, column, renderer, model, titer,
4634 data = None):
4635 '''When a row is added, set properties for avatar renderer'''
4636 theme = gajim.config.get('roster_theme')
4637 type_ = model[titer][C_TYPE]
4638 if type_ == 'group':
4639 renderer.set_property('visible', False)
4640 return
4642 # allocate space for the icon only if needed
4643 if model[titer][C_TUNE_PIXBUF]:
4644 renderer.set_property('visible', True)
4645 else:
4646 renderer.set_property('visible', False)
4647 if type_ == 'account':
4648 color = gajim.config.get_per('themes', theme,
4649 'accountbgcolor')
4650 if color:
4651 renderer.set_property('cell-background', color)
4652 else:
4653 self.set_renderer_color(renderer,
4654 gtk.STATE_ACTIVE)
4655 # align pixbuf to the right)
4656 renderer.set_property('xalign', 1)
4657 # prevent type_ = None, see http://trac.gajim.org/ticket/2534
4658 elif type_:
4659 if not model[titer][C_JID] \
4660 or not model[titer][C_ACCOUNT]:
4661 # This can append at the moment we add the row
4662 return
4663 jid = model[titer][C_JID].decode('utf-8')
4664 account = model[titer][C_ACCOUNT].decode('utf-8')
4665 if jid in gajim.newly_added[account]:
4666 renderer.set_property('cell-background',
4667 gajim.config.get(
4668 'just_connected_bg_color'))
4669 elif jid in gajim.to_be_removed[account]:
4670 renderer.set_property('cell-background',
4671 gajim.config.get(
4672 'just_disconnected_bg_color'))
4673 else:
4674 color = gajim.config.get_per('themes',
4675 theme, 'contactbgcolor')
4676 if color:
4677 renderer.set_property(
4678 'cell-background', color)
4679 else:
4680 renderer.set_property(
4681 'cell-background', None)
4682 # align pixbuf to the right
4683 renderer.set_property('xalign', 1)
4686 def _fill_avatar_pixbuf_renderer(self, column, renderer, model, titer,
4687 data = None):
4688 '''When a row is added, set properties for avatar renderer'''
4689 theme = gajim.config.get('roster_theme')
4690 type_ = model[titer][C_TYPE]
4691 if type_ in ('group', 'account'):
4692 renderer.set_property('visible', False)
4693 return
4695 # allocate space for the icon only if needed
4696 if model[titer][C_AVATAR_PIXBUF] or \
4697 gajim.config.get('avatar_position_in_roster') == 'left':
4698 renderer.set_property('visible', True)
4699 else:
4700 renderer.set_property('visible', False)
4701 if type_: # prevent type_ = None, see http://trac.gajim.org/ticket/2534
4702 if not model[titer][C_JID] or not model[titer][C_ACCOUNT]:
4703 # This can append at the moment we add the row
4704 return
4705 jid = model[titer][C_JID].decode('utf-8')
4706 account = model[titer][C_ACCOUNT].decode('utf-8')
4707 if jid in gajim.newly_added[account]:
4708 renderer.set_property('cell-background', gajim.config.get(
4709 'just_connected_bg_color'))
4710 elif jid in gajim.to_be_removed[account]:
4711 renderer.set_property('cell-background', gajim.config.get(
4712 'just_disconnected_bg_color'))
4713 else:
4714 color = gajim.config.get_per('themes', theme, 'contactbgcolor')
4715 if color:
4716 renderer.set_property('cell-background', color)
4717 else:
4718 renderer.set_property('cell-background', None)
4719 if gajim.config.get('avatar_position_in_roster') == 'left':
4720 renderer.set_property('width', gajim.config.get('roster_avatar_width'))
4721 renderer.set_property('xalign', 0.5)
4722 else:
4723 renderer.set_property('xalign', 1) # align pixbuf to the right
4725 def _fill_padlock_pixbuf_renderer(self, column, renderer, model, titer,
4726 data = None):
4727 '''When a row is added, set properties for padlock renderer'''
4728 theme = gajim.config.get('roster_theme')
4729 type_ = model[titer][C_TYPE]
4730 # allocate space for the icon only if needed
4731 if type_ == 'account' and model[titer][C_PADLOCK_PIXBUF]:
4732 renderer.set_property('visible', True)
4733 color = gajim.config.get_per('themes', theme, 'accountbgcolor')
4734 if color:
4735 renderer.set_property('cell-background', color)
4736 else:
4737 self.set_renderer_color(renderer, gtk.STATE_ACTIVE)
4738 renderer.set_property('xalign', 1) # align pixbuf to the right
4739 else:
4740 renderer.set_property('visible', False)
4742 ################################################################################
4743 ### Everything about building menus
4744 ### FIXME: We really need to make it simpler! 1465 lines are a few to much....
4745 ################################################################################
4747 def make_menu(self, force=False):
4748 '''create the main window\'s menus'''
4749 if not force and not self.actions_menu_needs_rebuild:
4750 return
4751 new_chat_menuitem = self.xml.get_widget('new_chat_menuitem')
4752 single_message_menuitem = self.xml.get_widget(
4753 'send_single_message_menuitem')
4754 join_gc_menuitem = self.xml.get_widget('join_gc_menuitem')
4755 muc_icon = gtkgui_helpers.load_icon('muc_active')
4756 if muc_icon:
4757 join_gc_menuitem.set_image(muc_icon)
4758 add_new_contact_menuitem = self.xml.get_widget('add_new_contact_menuitem')
4759 service_disco_menuitem = self.xml.get_widget('service_disco_menuitem')
4760 advanced_menuitem = self.xml.get_widget('advanced_menuitem')
4761 profile_avatar_menuitem = self.xml.get_widget('profile_avatar_menuitem')
4763 # destroy old advanced menus
4764 for m in self.advanced_menus:
4765 m.destroy()
4767 # make it sensitive. it is insensitive only if no accounts are *available*
4768 advanced_menuitem.set_sensitive(True)
4770 if self.add_new_contact_handler_id:
4771 add_new_contact_menuitem.handler_disconnect(
4772 self.add_new_contact_handler_id)
4773 self.add_new_contact_handler_id = None
4775 if self.service_disco_handler_id:
4776 service_disco_menuitem.handler_disconnect(
4777 self.service_disco_handler_id)
4778 self.service_disco_handler_id = None
4780 if self.new_chat_menuitem_handler_id:
4781 new_chat_menuitem.handler_disconnect(
4782 self.new_chat_menuitem_handler_id)
4783 self.new_chat_menuitem_handler_id = None
4785 if self.single_message_menuitem_handler_id:
4786 single_message_menuitem.handler_disconnect(
4787 self.single_message_menuitem_handler_id)
4788 self.single_message_menuitem_handler_id = None
4790 if self.profile_avatar_menuitem_handler_id:
4791 profile_avatar_menuitem.handler_disconnect(
4792 self.profile_avatar_menuitem_handler_id)
4793 self.profile_avatar_menuitem_handler_id = None
4795 # remove the existing submenus
4796 add_new_contact_menuitem.remove_submenu()
4797 service_disco_menuitem.remove_submenu()
4798 join_gc_menuitem.remove_submenu()
4799 single_message_menuitem.remove_submenu()
4800 new_chat_menuitem.remove_submenu()
4801 advanced_menuitem.remove_submenu()
4802 profile_avatar_menuitem.remove_submenu()
4804 # remove the existing accelerator
4805 if self.have_new_chat_accel:
4806 ag = gtk.accel_groups_from_object(self.window)[0]
4807 new_chat_menuitem.remove_accelerator(ag, gtk.keysyms.n,
4808 gtk.gdk.CONTROL_MASK)
4809 self.have_new_chat_accel = False
4811 gc_sub_menu = gtk.Menu() # gc is always a submenu
4812 join_gc_menuitem.set_submenu(gc_sub_menu)
4814 connected_accounts = gajim.get_number_of_connected_accounts()
4816 connected_accounts_with_private_storage = 0
4818 # items that get shown whether an account is zeroconf or not
4819 accounts_list = sorted(gajim.contacts.get_accounts())
4820 if connected_accounts > 1: # 2 or more accounts? make submenus
4821 new_chat_sub_menu = gtk.Menu()
4823 for account in accounts_list:
4824 if gajim.connections[account].connected <= 1:
4825 # if offline or connecting
4826 continue
4828 # new chat
4829 new_chat_item = gtk.MenuItem(_('using account %s') % account,
4830 False)
4831 new_chat_sub_menu.append(new_chat_item)
4832 new_chat_item.connect('activate',
4833 self.on_new_chat_menuitem_activate, account)
4835 new_chat_menuitem.set_submenu(new_chat_sub_menu)
4836 new_chat_sub_menu.show_all()
4838 elif connected_accounts == 1: # user has only one account
4839 for account in gajim.connections:
4840 if gajim.account_is_connected(account): # THE connected account
4841 # new chat
4842 if not self.new_chat_menuitem_handler_id:
4843 self.new_chat_menuitem_handler_id = new_chat_menuitem.\
4844 connect('activate', self.on_new_chat_menuitem_activate,
4845 account)
4847 break
4849 # menu items that don't apply to zeroconf connections
4850 if connected_accounts == 1 or (connected_accounts == 2 and \
4851 gajim.zeroconf_is_connected()):
4852 # only one 'real' (non-zeroconf) account is connected, don't need submenus
4854 for account in accounts_list:
4855 if gajim.account_is_connected(account) and \
4856 not gajim.config.get_per('accounts', account, 'is_zeroconf'):
4857 # gc
4858 if gajim.connections[account].private_storage_supported:
4859 connected_accounts_with_private_storage += 1
4860 self.add_bookmarks_list(gc_sub_menu, account)
4861 gc_sub_menu.show_all()
4862 # add
4863 if not self.add_new_contact_handler_id:
4864 self.add_new_contact_handler_id =\
4865 add_new_contact_menuitem.connect(
4866 'activate', self.on_add_new_contact, account)
4867 # disco
4868 if not self.service_disco_handler_id:
4869 self.service_disco_handler_id = service_disco_menuitem.\
4870 connect('activate',
4871 self.on_service_disco_menuitem_activate, account)
4873 # single message
4874 if not self.single_message_menuitem_handler_id:
4875 self.single_message_menuitem_handler_id = \
4876 single_message_menuitem.connect('activate', \
4877 self.on_send_single_message_menuitem_activate, account)
4879 # new chat accel
4880 if not self.have_new_chat_accel:
4881 ag = gtk.accel_groups_from_object(self.window)[0]
4882 new_chat_menuitem.add_accelerator('activate', ag,
4883 gtk.keysyms.n, gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
4884 self.have_new_chat_accel = True
4886 break # No other account connected
4887 else:
4888 # 2 or more 'real' accounts are connected, make submenus
4889 single_message_sub_menu = gtk.Menu()
4890 add_sub_menu = gtk.Menu()
4891 disco_sub_menu = gtk.Menu()
4893 for account in accounts_list:
4894 if gajim.connections[account].connected <= 1 or \
4895 gajim.config.get_per('accounts', account, 'is_zeroconf'):
4896 # skip account if it's offline or connecting or is zeroconf
4897 continue
4899 # single message
4900 single_message_item = gtk.MenuItem(_('using account %s') % account,
4901 False)
4902 single_message_sub_menu.append(single_message_item)
4903 single_message_item.connect('activate',
4904 self.on_send_single_message_menuitem_activate, account)
4906 # join gc
4907 if gajim.connections[account].private_storage_supported:
4908 connected_accounts_with_private_storage += 1
4909 gc_item = gtk.MenuItem(_('using account %s') % account, False)
4910 gc_sub_menu.append(gc_item)
4911 gc_menuitem_menu = gtk.Menu()
4912 self.add_bookmarks_list(gc_menuitem_menu, account)
4913 gc_item.set_submenu(gc_menuitem_menu)
4915 # add
4916 add_item = gtk.MenuItem(_('to %s account') % account, False)
4917 add_sub_menu.append(add_item)
4918 add_item.connect('activate', self.on_add_new_contact, account)
4920 # disco
4921 disco_item = gtk.MenuItem(_('using %s account') % account, False)
4922 disco_sub_menu.append(disco_item)
4923 disco_item.connect('activate',
4924 self.on_service_disco_menuitem_activate, account)
4926 single_message_menuitem.set_submenu(single_message_sub_menu)
4927 single_message_sub_menu.show_all()
4928 gc_sub_menu.show_all()
4929 add_new_contact_menuitem.set_submenu(add_sub_menu)
4930 add_sub_menu.show_all()
4931 service_disco_menuitem.set_submenu(disco_sub_menu)
4932 disco_sub_menu.show_all()
4934 if connected_accounts == 0:
4935 # no connected accounts, make the menuitems insensitive
4936 for item in (new_chat_menuitem, join_gc_menuitem,\
4937 add_new_contact_menuitem, service_disco_menuitem,\
4938 single_message_menuitem):
4939 item.set_sensitive(False)
4940 else: # we have one or more connected accounts
4941 for item in (new_chat_menuitem, join_gc_menuitem,
4942 add_new_contact_menuitem, service_disco_menuitem,
4943 single_message_menuitem):
4944 item.set_sensitive(True)
4945 # disable some fields if only local account is there
4946 if connected_accounts == 1:
4947 for account in gajim.connections:
4948 if gajim.account_is_connected(account) and \
4949 gajim.connections[account].is_zeroconf:
4950 for item in (join_gc_menuitem, add_new_contact_menuitem,
4951 service_disco_menuitem, single_message_menuitem):
4952 item.set_sensitive(False)
4954 # Manage GC bookmarks
4955 newitem = gtk.SeparatorMenuItem() # separator
4956 gc_sub_menu.append(newitem)
4958 newitem = gtk.ImageMenuItem(_('_Manage Bookmarks...'))
4959 img = gtk.image_new_from_stock(gtk.STOCK_PREFERENCES,
4960 gtk.ICON_SIZE_MENU)
4961 newitem.set_image(img)
4962 newitem.connect('activate', self.on_manage_bookmarks_menuitem_activate)
4963 gc_sub_menu.append(newitem)
4964 gc_sub_menu.show_all()
4965 if connected_accounts_with_private_storage == 0:
4966 newitem.set_sensitive(False)
4968 connected_accounts_with_vcard = []
4969 for account in gajim.connections:
4970 if gajim.account_is_connected(account) and \
4971 gajim.connections[account].vcard_supported:
4972 connected_accounts_with_vcard.append(account)
4973 if len(connected_accounts_with_vcard) > 1:
4974 # 2 or more accounts? make submenus
4975 profile_avatar_sub_menu = gtk.Menu()
4976 for account in connected_accounts_with_vcard:
4977 # profile, avatar
4978 profile_avatar_item = gtk.MenuItem(_('of account %s') % account,
4979 False)
4980 profile_avatar_sub_menu.append(profile_avatar_item)
4981 profile_avatar_item.connect('activate',
4982 self.on_profile_avatar_menuitem_activate, account)
4983 profile_avatar_menuitem.set_submenu(profile_avatar_sub_menu)
4984 profile_avatar_sub_menu.show_all()
4985 elif len(connected_accounts_with_vcard) == 1: # user has only one account
4986 account = connected_accounts_with_vcard[0]
4987 # profile, avatar
4988 if not self.profile_avatar_menuitem_handler_id:
4989 self.profile_avatar_menuitem_handler_id = \
4990 profile_avatar_menuitem.connect('activate',
4991 self.on_profile_avatar_menuitem_activate, account)
4993 if len(connected_accounts_with_vcard) == 0:
4994 profile_avatar_menuitem.set_sensitive(False)
4995 else:
4996 profile_avatar_menuitem.set_sensitive(True)
4998 # Advanced Actions
4999 if len(gajim.connections) == 0: # user has no accounts
5000 advanced_menuitem.set_sensitive(False)
5001 elif len(gajim.connections) == 1: # we have one acccount
5002 account = gajim.connections.keys()[0]
5003 advanced_menuitem_menu = self.get_and_connect_advanced_menuitem_menu(
5004 account)
5005 self.advanced_menus.append(advanced_menuitem_menu)
5007 self.add_history_manager_menuitem(advanced_menuitem_menu)
5009 advanced_menuitem.set_submenu(advanced_menuitem_menu)
5010 advanced_menuitem_menu.show_all()
5011 else: # user has *more* than one account : build advanced submenus
5012 advanced_sub_menu = gtk.Menu()
5013 accounts = [] # Put accounts in a list to sort them
5014 for account in gajim.connections:
5015 accounts.append(account)
5016 accounts.sort()
5017 for account in accounts:
5018 advanced_item = gtk.MenuItem(_('for account %s') % account, False)
5019 advanced_sub_menu.append(advanced_item)
5020 advanced_menuitem_menu = \
5021 self.get_and_connect_advanced_menuitem_menu(account)
5022 self.advanced_menus.append(advanced_menuitem_menu)
5023 advanced_item.set_submenu(advanced_menuitem_menu)
5025 self.add_history_manager_menuitem(advanced_sub_menu)
5027 advanced_menuitem.set_submenu(advanced_sub_menu)
5028 advanced_sub_menu.show_all()
5030 self.actions_menu_needs_rebuild = False
5032 def build_account_menu(self, account):
5033 # we have to create our own set of icons for the menu
5034 # using self.jabber_status_images is poopoo
5035 iconset = gajim.config.get('iconset')
5036 path = os.path.join(helpers.get_iconset_path(iconset), '16x16')
5037 state_images = gtkgui_helpers.load_iconset(path)
5039 if not gajim.config.get_per('accounts', account, 'is_zeroconf'):
5040 xml = gtkgui_helpers.get_glade('account_context_menu.glade')
5041 account_context_menu = xml.get_widget('account_context_menu')
5043 status_menuitem = xml.get_widget('status_menuitem')
5044 start_chat_menuitem = xml.get_widget('start_chat_menuitem')
5045 join_group_chat_menuitem = xml.get_widget('join_group_chat_menuitem')
5046 muc_icon = gtkgui_helpers.load_icon('muc_active')
5047 if muc_icon:
5048 join_group_chat_menuitem.set_image(muc_icon)
5049 open_gmail_inbox_menuitem = xml.get_widget('open_gmail_inbox_menuitem')
5050 add_contact_menuitem = xml.get_widget('add_contact_menuitem')
5051 service_discovery_menuitem = xml.get_widget(
5052 'service_discovery_menuitem')
5053 execute_command_menuitem = xml.get_widget('execute_command_menuitem')
5054 edit_account_menuitem = xml.get_widget('edit_account_menuitem')
5055 sub_menu = gtk.Menu()
5056 status_menuitem.set_submenu(sub_menu)
5058 for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'):
5059 uf_show = helpers.get_uf_show(show, use_mnemonic=True)
5060 item = gtk.ImageMenuItem(uf_show)
5061 icon = state_images[show]
5062 item.set_image(icon)
5063 sub_menu.append(item)
5064 con = gajim.connections[account]
5065 if show == 'invisible' and con.connected > 1 and \
5066 not con.privacy_rules_supported:
5067 item.set_sensitive(False)
5068 else:
5069 item.connect('activate', self.change_status, account, show)
5071 item = gtk.SeparatorMenuItem()
5072 sub_menu.append(item)
5074 item = gtk.ImageMenuItem(_('_Change Status Message'))
5075 path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'kbd_input.png')
5076 img = gtk.Image()
5077 img.set_from_file(path)
5078 item.set_image(img)
5079 sub_menu.append(item)
5080 item.connect('activate', self.on_change_status_message_activate,
5081 account)
5082 if gajim.connections[account].connected < 2:
5083 item.set_sensitive(False)
5085 item = gtk.SeparatorMenuItem()
5086 sub_menu.append(item)
5088 uf_show = helpers.get_uf_show('offline', use_mnemonic=True)
5089 item = gtk.ImageMenuItem(uf_show)
5090 icon = state_images['offline']
5091 item.set_image(icon)
5092 sub_menu.append(item)
5093 item.connect('activate', self.change_status, account, 'offline')
5095 pep_menuitem = xml.get_widget('pep_menuitem')
5096 if gajim.connections[account].pep_supported:
5097 have_tune = gajim.config.get_per('accounts', account,
5098 'publish_tune')
5099 pep_submenu = gtk.Menu()
5100 pep_menuitem.set_submenu(pep_submenu)
5101 item = gtk.CheckMenuItem(_('Publish Tune'))
5102 pep_submenu.append(item)
5103 if not dbus_support.supported:
5104 item.set_sensitive(False)
5105 else:
5106 item.set_active(have_tune)
5107 item.connect('toggled', self.on_publish_tune_toggled, account)
5109 pep_config = gtk.ImageMenuItem(_('Configure Services...'))
5110 item = gtk.SeparatorMenuItem()
5111 pep_submenu.append(item)
5112 pep_config.set_sensitive(True)
5113 pep_submenu.append(pep_config)
5114 pep_config.connect('activate',
5115 self.on_pep_services_menuitem_activate, account)
5116 img = gtk.image_new_from_stock(gtk.STOCK_PREFERENCES,
5117 gtk.ICON_SIZE_MENU)
5118 pep_config.set_image(img)
5120 else:
5121 pep_menuitem.set_sensitive(False)
5123 if not gajim.connections[account].gmail_url:
5124 open_gmail_inbox_menuitem.set_no_show_all(True)
5125 open_gmail_inbox_menuitem.hide()
5126 else:
5127 open_gmail_inbox_menuitem.connect('activate',
5128 self.on_open_gmail_inbox, account)
5130 edit_account_menuitem.connect('activate', self.on_edit_account,
5131 account)
5132 add_contact_menuitem.connect('activate', self.on_add_new_contact,
5133 account)
5134 service_discovery_menuitem.connect('activate',
5135 self.on_service_disco_menuitem_activate, account)
5136 hostname = gajim.config.get_per('accounts', account, 'hostname')
5137 contact = gajim.contacts.create_contact(jid=hostname) # Fake contact
5138 execute_command_menuitem.connect('activate',
5139 self.on_execute_command, contact, account)
5141 start_chat_menuitem.connect('activate',
5142 self.on_new_chat_menuitem_activate, account)
5144 gc_sub_menu = gtk.Menu() # gc is always a submenu
5145 join_group_chat_menuitem.set_submenu(gc_sub_menu)
5146 self.add_bookmarks_list(gc_sub_menu, account)
5148 # make some items insensitive if account is offline
5149 if gajim.connections[account].connected < 2:
5150 for widget in (add_contact_menuitem, service_discovery_menuitem,
5151 join_group_chat_menuitem, execute_command_menuitem, pep_menuitem,
5152 start_chat_menuitem):
5153 widget.set_sensitive(False)
5154 else:
5155 xml = gtkgui_helpers.get_glade('zeroconf_context_menu.glade')
5156 account_context_menu = xml.get_widget('zeroconf_context_menu')
5158 status_menuitem = xml.get_widget('status_menuitem')
5159 zeroconf_properties_menuitem = xml.get_widget(
5160 'zeroconf_properties_menuitem')
5161 sub_menu = gtk.Menu()
5162 status_menuitem.set_submenu(sub_menu)
5164 for show in ('online', 'away', 'dnd', 'invisible'):
5165 uf_show = helpers.get_uf_show(show, use_mnemonic=True)
5166 item = gtk.ImageMenuItem(uf_show)
5167 icon = state_images[show]
5168 item.set_image(icon)
5169 sub_menu.append(item)
5170 item.connect('activate', self.change_status, account, show)
5172 item = gtk.SeparatorMenuItem()
5173 sub_menu.append(item)
5175 item = gtk.ImageMenuItem(_('_Change Status Message'))
5176 path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'kbd_input.png')
5177 img = gtk.Image()
5178 img.set_from_file(path)
5179 item.set_image(img)
5180 sub_menu.append(item)
5181 item.connect('activate', self.on_change_status_message_activate,
5182 account)
5183 if gajim.connections[account].connected < 2:
5184 item.set_sensitive(False)
5186 uf_show = helpers.get_uf_show('offline', use_mnemonic=True)
5187 item = gtk.ImageMenuItem(uf_show)
5188 icon = state_images['offline']
5189 item.set_image(icon)
5190 sub_menu.append(item)
5191 item.connect('activate', self.change_status, account, 'offline')
5193 zeroconf_properties_menuitem.connect('activate',
5194 self.on_zeroconf_properties, account)
5195 #gc_sub_menu = gtk.Menu() # gc is always a submenu
5196 #join_group_chat_menuitem.set_submenu(gc_sub_menu)
5197 #self.add_bookmarks_list(gc_sub_menu, account)
5198 #new_message_menuitem.connect('activate',
5199 # self.on_new_message_menuitem_activate, account)
5201 # make some items insensitive if account is offline
5202 #if gajim.connections[account].connected < 2:
5203 # for widget in [join_group_chat_menuitem, new_message_menuitem]:
5204 # widget.set_sensitive(False)
5205 # new_message_menuitem.set_sensitive(False)
5207 return account_context_menu
5209 def make_account_menu(self, event, titer):
5210 '''Make account's popup menu'''
5211 model = self.modelfilter
5212 account = model[titer][C_ACCOUNT].decode('utf-8')
5214 if account != 'all': # not in merged mode
5215 menu = self.build_account_menu(account)
5216 else:
5217 menu = gtk.Menu()
5218 iconset = gajim.config.get('iconset')
5219 path = os.path.join(helpers.get_iconset_path(iconset), '16x16')
5220 accounts = [] # Put accounts in a list to sort them
5221 for account in gajim.connections:
5222 accounts.append(account)
5223 accounts.sort()
5224 for account in accounts:
5225 state_images = gtkgui_helpers.load_iconset(path)
5226 item = gtk.ImageMenuItem(account)
5227 show = gajim.SHOW_LIST[gajim.connections[account].connected]
5228 icon = state_images[show]
5229 item.set_image(icon)
5230 account_menu = self.build_account_menu(account)
5231 item.set_submenu(account_menu)
5232 menu.append(item)
5234 event_button = gtkgui_helpers.get_possible_button_event(event)
5236 menu.attach_to_widget(self.tree, None)
5237 menu.connect('selection-done', gtkgui_helpers.destroy_widget)
5238 menu.show_all()
5239 menu.popup(None, None, None, event_button, event.time)
5241 def make_group_menu(self, event, titer):
5242 '''Make group's popup menu'''
5243 model = self.modelfilter
5244 path = model.get_path(titer)
5245 group = model[titer][C_JID].decode('utf-8')
5246 account = model[titer][C_ACCOUNT].decode('utf-8')
5248 list_ = [] # list of (jid, account) tuples
5249 list_online = [] # list of (jid, account) tuples
5251 group = model[titer][C_JID]
5252 for jid in gajim.contacts.get_jid_list(account):
5253 contact = gajim.contacts.get_contact_with_highest_priority(account,
5254 jid)
5255 if group in contact.get_shown_groups():
5256 if contact.show not in ('offline', 'error'):
5257 list_online.append((contact, account))
5258 list_.append((contact, account))
5259 menu = gtk.Menu()
5261 # Make special context menu if group is Groupchats
5262 if group == _('Groupchats'):
5263 maximize_menuitem = gtk.ImageMenuItem(_('_Maximize All'))
5264 icon = gtk.image_new_from_stock(gtk.STOCK_GOTO_TOP, gtk.ICON_SIZE_MENU)
5265 maximize_menuitem.set_image(icon)
5266 maximize_menuitem.connect('activate', self.on_all_groupchat_maximized,\
5267 list_)
5268 menu.append(maximize_menuitem)
5269 else:
5270 # Send Group Message
5271 send_group_message_item = gtk.ImageMenuItem(_('Send Group M_essage'))
5272 icon = gtk.image_new_from_stock(gtk.STOCK_NEW, gtk.ICON_SIZE_MENU)
5273 send_group_message_item.set_image(icon)
5275 send_group_message_submenu = gtk.Menu()
5276 send_group_message_item.set_submenu(send_group_message_submenu)
5277 menu.append(send_group_message_item)
5279 group_message_to_all_item = gtk.MenuItem(_('To all users'))
5280 send_group_message_submenu.append(group_message_to_all_item)
5282 group_message_to_all_online_item = gtk.MenuItem(
5283 _('To all online users'))
5284 send_group_message_submenu.append(group_message_to_all_online_item)
5286 group_message_to_all_online_item.connect('activate',
5287 self.on_send_single_message_menuitem_activate, account, list_online)
5288 group_message_to_all_item.connect('activate',
5289 self.on_send_single_message_menuitem_activate, account, list_)
5291 # Invite to
5292 invite_menuitem = gtk.ImageMenuItem(_('In_vite to'))
5293 muc_icon = gtkgui_helpers.load_icon('muc_active')
5294 if muc_icon:
5295 invite_menuitem.set_image(muc_icon)
5297 gui_menu_builder.build_invite_submenu(invite_menuitem, list_online)
5298 menu.append(invite_menuitem)
5300 # Send Custom Status
5301 send_custom_status_menuitem = gtk.ImageMenuItem(
5302 _('Send Cus_tom Status'))
5303 # add a special img for this menuitem
5304 if helpers.group_is_blocked(account, group):
5305 send_custom_status_menuitem.set_image(gtkgui_helpers.load_icon(
5306 'offline'))
5307 send_custom_status_menuitem.set_sensitive(False)
5308 else:
5309 icon = gtk.image_new_from_stock(gtk.STOCK_NETWORK,
5310 gtk.ICON_SIZE_MENU)
5311 send_custom_status_menuitem.set_image(icon)
5312 status_menuitems = gtk.Menu()
5313 send_custom_status_menuitem.set_submenu(status_menuitems)
5314 iconset = gajim.config.get('iconset')
5315 path = os.path.join(helpers.get_iconset_path(iconset), '16x16')
5316 for s in ('online', 'chat', 'away', 'xa', 'dnd', 'offline'):
5317 # icon MUST be different instance for every item
5318 state_images = gtkgui_helpers.load_iconset(path)
5319 status_menuitem = gtk.ImageMenuItem(helpers.get_uf_show(s))
5320 status_menuitem.connect('activate', self.on_send_custom_status,
5321 list_, s, group)
5322 icon = state_images[s]
5323 status_menuitem.set_image(icon)
5324 status_menuitems.append(status_menuitem)
5325 menu.append(send_custom_status_menuitem)
5327 # there is no singlemessage and custom status for zeroconf
5328 if gajim.config.get_per('accounts', account, 'is_zeroconf'):
5329 send_custom_status_menuitem.set_sensitive(False)
5330 send_group_message_item.set_sensitive(False)
5332 if not group in helpers.special_groups:
5333 item = gtk.SeparatorMenuItem() # separator
5334 menu.append(item)
5336 # Rename
5337 rename_item = gtk.ImageMenuItem(_('Re_name'))
5338 # add a special img for rename menuitem
5339 path_to_kbd_input_img = os.path.join(gajim.DATA_DIR, 'pixmaps',
5340 'kbd_input.png')
5341 img = gtk.Image()
5342 img.set_from_file(path_to_kbd_input_img)
5343 rename_item.set_image(img)
5344 menu.append(rename_item)
5345 rename_item.connect('activate', self.on_rename, 'group', group,
5346 account)
5348 # Block group
5349 is_blocked = False
5350 if self.regroup:
5351 for g_account in gajim.connections:
5352 if helpers.group_is_blocked(g_account, group):
5353 is_blocked = True
5354 else:
5355 if helpers.group_is_blocked(account, group):
5356 is_blocked = True
5358 if is_blocked and gajim.connections[account].privacy_rules_supported:
5359 unblock_menuitem = gtk.ImageMenuItem(_('_Unblock'))
5360 icon = gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_MENU)
5361 unblock_menuitem.set_image(icon)
5362 unblock_menuitem.connect('activate', self.on_unblock, list_, group)
5363 menu.append(unblock_menuitem)
5364 else:
5365 block_menuitem = gtk.ImageMenuItem(_('_Block'))
5366 icon = gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_MENU)
5367 block_menuitem.set_image(icon)
5368 block_menuitem.connect('activate', self.on_block, list_, group)
5369 menu.append(block_menuitem)
5370 if not gajim.connections[account].privacy_rules_supported:
5371 block_menuitem.set_sensitive(False)
5373 # Remove group
5374 remove_item = gtk.ImageMenuItem(_('_Remove'))
5375 icon = gtk.image_new_from_stock(gtk.STOCK_REMOVE, gtk.ICON_SIZE_MENU)
5376 remove_item.set_image(icon)
5377 menu.append(remove_item)
5378 remove_item.connect('activate', self.on_remove_group_item_activated,
5379 group, account)
5381 # unsensitive if account is not connected
5382 if gajim.connections[account].connected < 2:
5383 rename_item.set_sensitive(False)
5385 # General group cannot be changed
5386 if group == _('General'):
5387 rename_item.set_sensitive(False)
5388 block_menuitem.set_sensitive(False)
5389 remove_item.set_sensitive(False)
5391 event_button = gtkgui_helpers.get_possible_button_event(event)
5393 menu.attach_to_widget(self.tree, None)
5394 menu.connect('selection-done', gtkgui_helpers.destroy_widget)
5395 menu.show_all()
5396 menu.popup(None, None, None, event_button, event.time)
5398 def make_contact_menu(self, event, titer):
5399 '''Make contact\'s popup menu'''
5400 model = self.modelfilter
5401 jid = model[titer][C_JID].decode('utf-8')
5402 tree_path = model.get_path(titer)
5403 account = model[titer][C_ACCOUNT].decode('utf-8')
5404 contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
5405 menu = gui_menu_builder.get_contact_menu(contact, account)
5406 event_button = gtkgui_helpers.get_possible_button_event(event)
5407 menu.attach_to_widget(self.tree, None)
5408 menu.popup(None, None, None, event_button, event.time)
5410 def make_multiple_contact_menu(self, event, iters):
5411 '''Make group's popup menu'''
5412 model = self.modelfilter
5413 list_ = [] # list of (jid, account) tuples
5414 one_account_offline = False
5415 is_blocked = True
5416 privacy_rules_supported = True
5417 for titer in iters:
5418 jid = model[titer][C_JID].decode('utf-8')
5419 account = model[titer][C_ACCOUNT].decode('utf-8')
5420 if gajim.connections[account].connected < 2:
5421 one_account_offline = True
5422 if not gajim.connections[account].privacy_rules_supported:
5423 privacy_rules_supported = False
5424 contact = gajim.contacts.get_contact_with_highest_priority(account,
5425 jid)
5426 if helpers.jid_is_blocked(account, jid):
5427 is_blocked = False
5428 list_.append((contact, account))
5430 menu = gtk.Menu()
5431 account = None
5432 for (contact, current_account) in list_:
5433 # check that we use the same account for every sender
5434 if account is not None and account != current_account:
5435 account = None
5436 break
5437 account = current_account
5438 if account is not None:
5439 send_group_message_item = gtk.ImageMenuItem(_('Send Group M_essage'))
5440 icon = gtk.image_new_from_stock(gtk.STOCK_NEW, gtk.ICON_SIZE_MENU)
5441 send_group_message_item.set_image(icon)
5442 menu.append(send_group_message_item)
5443 send_group_message_item.connect('activate',
5444 self.on_send_single_message_menuitem_activate, account, list_)
5446 # Invite to Groupchat
5447 invite_item = gtk.ImageMenuItem(_('In_vite to'))
5448 muc_icon = gtkgui_helpers.load_icon('muc_active')
5449 if muc_icon:
5450 invite_item.set_image(muc_icon)
5452 gui_menu_builder.build_invite_submenu(invite_item, list_)
5453 menu.append(invite_item)
5455 item = gtk.SeparatorMenuItem() # separator
5456 menu.append(item)
5458 # Manage Transport submenu
5459 item = gtk.ImageMenuItem(_('_Manage Contacts'))
5460 icon = gtk.image_new_from_stock(gtk.STOCK_PROPERTIES, gtk.ICON_SIZE_MENU)
5461 item.set_image(icon)
5462 manage_contacts_submenu = gtk.Menu()
5463 item.set_submenu(manage_contacts_submenu)
5464 menu.append(item)
5466 # Edit Groups
5467 edit_groups_item = gtk.ImageMenuItem(_('Edit _Groups'))
5468 icon = gtk.image_new_from_stock(gtk.STOCK_EDIT, gtk.ICON_SIZE_MENU)
5469 edit_groups_item.set_image(icon)
5470 manage_contacts_submenu.append(edit_groups_item)
5471 edit_groups_item.connect('activate', self.on_edit_groups, list_)
5473 item = gtk.SeparatorMenuItem() # separator
5474 manage_contacts_submenu.append(item)
5476 # Block
5477 if is_blocked and privacy_rules_supported:
5478 unblock_menuitem = gtk.ImageMenuItem(_('_Unblock'))
5479 icon = gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_MENU)
5480 unblock_menuitem.set_image(icon)
5481 unblock_menuitem.connect('activate', self.on_unblock, list_)
5482 manage_contacts_submenu.append(unblock_menuitem)
5483 else:
5484 block_menuitem = gtk.ImageMenuItem(_('_Block'))
5485 icon = gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_MENU)
5486 block_menuitem.set_image(icon)
5487 block_menuitem.connect('activate', self.on_block, list_)
5488 manage_contacts_submenu.append(block_menuitem)
5490 if not privacy_rules_supported:
5491 block_menuitem.set_sensitive(False)
5493 # Remove
5494 remove_item = gtk.ImageMenuItem(_('_Remove'))
5495 icon = gtk.image_new_from_stock(gtk.STOCK_REMOVE, gtk.ICON_SIZE_MENU)
5496 remove_item.set_image(icon)
5497 manage_contacts_submenu.append(remove_item)
5498 remove_item.connect('activate', self.on_req_usub, list_)
5499 # unsensitive remove if one account is not connected
5500 if one_account_offline:
5501 remove_item.set_sensitive(False)
5503 event_button = gtkgui_helpers.get_possible_button_event(event)
5505 menu.attach_to_widget(self.tree, None)
5506 menu.connect('selection-done', gtkgui_helpers.destroy_widget)
5507 menu.show_all()
5508 menu.popup(None, None, None, event_button, event.time)
5510 def make_transport_menu(self, event, titer):
5511 '''Make transport\'s popup menu'''
5512 model = self.modelfilter
5513 jid = model[titer][C_JID].decode('utf-8')
5514 path = model.get_path(titer)
5515 account = model[titer][C_ACCOUNT].decode('utf-8')
5516 contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
5517 menu = gtk.Menu()
5519 # Send single message
5520 item = gtk.ImageMenuItem(_('Send Single Message'))
5521 icon = gtk.image_new_from_stock(gtk.STOCK_NEW, gtk.ICON_SIZE_MENU)
5522 item.set_image(icon)
5523 item.connect('activate',
5524 self.on_send_single_message_menuitem_activate, account, contact)
5525 menu.append(item)
5527 blocked = False
5528 if helpers.jid_is_blocked(account, jid):
5529 blocked = True
5531 # Send Custom Status
5532 send_custom_status_menuitem = gtk.ImageMenuItem(_('Send Cus_tom Status'))
5533 # add a special img for this menuitem
5534 if blocked:
5535 send_custom_status_menuitem.set_image(gtkgui_helpers.load_icon(
5536 'offline'))
5537 send_custom_status_menuitem.set_sensitive(False)
5538 else:
5539 if account in gajim.interface.status_sent_to_users and \
5540 jid in gajim.interface.status_sent_to_users[account]:
5541 send_custom_status_menuitem.set_image(gtkgui_helpers.load_icon(
5542 gajim.interface.status_sent_to_users[account][jid]))
5543 else:
5544 icon = gtk.image_new_from_stock(gtk.STOCK_NETWORK,
5545 gtk.ICON_SIZE_MENU)
5546 send_custom_status_menuitem.set_image(icon)
5547 status_menuitems = gtk.Menu()
5548 send_custom_status_menuitem.set_submenu(status_menuitems)
5549 iconset = gajim.config.get('iconset')
5550 path = os.path.join(helpers.get_iconset_path(iconset), '16x16')
5551 for s in ('online', 'chat', 'away', 'xa', 'dnd', 'offline'):
5552 # icon MUST be different instance for every item
5553 state_images = gtkgui_helpers.load_iconset(path)
5554 status_menuitem = gtk.ImageMenuItem(helpers.get_uf_show(s))
5555 status_menuitem.connect('activate', self.on_send_custom_status,
5556 [(contact, account)], s)
5557 icon = state_images[s]
5558 status_menuitem.set_image(icon)
5559 status_menuitems.append(status_menuitem)
5560 menu.append(send_custom_status_menuitem)
5562 item = gtk.SeparatorMenuItem() # separator
5563 menu.append(item)
5565 # Execute Command
5566 item = gtk.ImageMenuItem(_('Execute Command...'))
5567 icon = gtk.image_new_from_stock(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_MENU)
5568 item.set_image(icon)
5569 menu.append(item)
5570 item.connect('activate', self.on_execute_command, contact, account,
5571 contact.resource)
5572 if gajim.account_is_disconnected(account):
5573 item.set_sensitive(False)
5575 # Manage Transport submenu
5576 item = gtk.ImageMenuItem(_('_Manage Transport'))
5577 icon = gtk.image_new_from_stock(gtk.STOCK_PROPERTIES, gtk.ICON_SIZE_MENU)
5578 item.set_image(icon)
5579 manage_transport_submenu = gtk.Menu()
5580 item.set_submenu(manage_transport_submenu)
5581 menu.append(item)
5583 # Modify Transport
5584 item = gtk.ImageMenuItem(_('_Modify Transport'))
5585 icon = gtk.image_new_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
5586 item.set_image(icon)
5587 manage_transport_submenu.append(item)
5588 item.connect('activate', self.on_edit_agent, contact, account)
5589 if gajim.account_is_disconnected(account):
5590 item.set_sensitive(False)
5592 # Rename
5593 item = gtk.ImageMenuItem(_('_Rename'))
5594 # add a special img for rename menuitem
5595 path_to_kbd_input_img = os.path.join(gajim.DATA_DIR, 'pixmaps',
5596 'kbd_input.png')
5597 img = gtk.Image()
5598 img.set_from_file(path_to_kbd_input_img)
5599 item.set_image(img)
5600 manage_transport_submenu.append(item)
5601 item.connect('activate', self.on_rename, 'agent', jid, account)
5602 if gajim.account_is_disconnected(account):
5603 item.set_sensitive(False)
5605 item = gtk.SeparatorMenuItem() # separator
5606 manage_transport_submenu.append(item)
5608 # Block
5609 if blocked:
5610 item = gtk.ImageMenuItem(_('_Unblock'))
5611 item.connect('activate', self.on_unblock, [(contact, account)])
5612 else:
5613 item = gtk.ImageMenuItem(_('_Block'))
5614 item.connect('activate', self.on_block, [(contact, account)])
5616 icon = gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_MENU)
5617 item.set_image(icon)
5618 manage_transport_submenu.append(item)
5619 if gajim.account_is_disconnected(account):
5620 item.set_sensitive(False)
5622 # Remove
5623 item = gtk.ImageMenuItem(_('_Remove'))
5624 icon = gtk.image_new_from_stock(gtk.STOCK_REMOVE, gtk.ICON_SIZE_MENU)
5625 item.set_image(icon)
5626 manage_transport_submenu.append(item)
5627 item.connect('activate', self.on_remove_agent, [(contact, account)])
5628 if gajim.account_is_disconnected(account):
5629 item.set_sensitive(False)
5631 item = gtk.SeparatorMenuItem() # separator
5632 menu.append(item)
5634 # Information
5635 information_menuitem = gtk.ImageMenuItem(_('_Information'))
5636 icon = gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU)
5637 information_menuitem.set_image(icon)
5638 menu.append(information_menuitem)
5639 information_menuitem.connect('activate', self.on_info, contact, account)
5642 event_button = gtkgui_helpers.get_possible_button_event(event)
5644 menu.attach_to_widget(self.tree, None)
5645 menu.connect('selection-done', gtkgui_helpers.destroy_widget)
5646 menu.show_all()
5647 menu.popup(None, None, None, event_button, event.time)
5649 def make_groupchat_menu(self, event, titer):
5650 model = self.modelfilter
5652 jid = model[titer][C_JID].decode('utf-8')
5653 account = model[titer][C_ACCOUNT].decode('utf-8')
5654 contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
5655 menu = gtk.Menu()
5657 if jid in gajim.interface.minimized_controls[account]:
5658 maximize_menuitem = gtk.ImageMenuItem(_('_Maximize'))
5659 icon = gtk.image_new_from_stock(gtk.STOCK_GOTO_TOP, gtk.ICON_SIZE_MENU)
5660 maximize_menuitem.set_image(icon)
5661 maximize_menuitem.connect('activate', self.on_groupchat_maximized, \
5662 jid, account)
5663 menu.append(maximize_menuitem)
5665 if not gajim.gc_connected[account].get(jid, False):
5666 connect_menuitem = gtk.ImageMenuItem(_('_Reconnect'))
5667 connect_icon = gtk.image_new_from_stock(gtk.STOCK_CONNECT, \
5668 gtk.ICON_SIZE_MENU)
5669 connect_menuitem.set_image(connect_icon)
5670 connect_menuitem.connect('activate', self.on_reconnect, jid, account)
5671 menu.append(connect_menuitem)
5672 disconnect_menuitem = gtk.ImageMenuItem(_('_Disconnect'))
5673 disconnect_icon = gtk.image_new_from_stock(gtk.STOCK_DISCONNECT, \
5674 gtk.ICON_SIZE_MENU)
5675 disconnect_menuitem.set_image(disconnect_icon)
5676 disconnect_menuitem.connect('activate', self.on_disconnect, jid, account)
5677 menu.append(disconnect_menuitem)
5679 item = gtk.SeparatorMenuItem() # separator
5680 menu.append(item)
5682 history_menuitem = gtk.ImageMenuItem(_('_History'))
5683 history_icon = gtk.image_new_from_stock(gtk.STOCK_JUSTIFY_FILL, \
5684 gtk.ICON_SIZE_MENU)
5685 history_menuitem.set_image(history_icon)
5686 history_menuitem .connect('activate', self.on_history, \
5687 contact, account)
5688 menu.append(history_menuitem)
5690 event_button = gtkgui_helpers.get_possible_button_event(event)
5692 menu.attach_to_widget(self.tree, None)
5693 menu.connect('selection-done', gtkgui_helpers.destroy_widget)
5694 menu.show_all()
5695 menu.popup(None, None, None, event_button, event.time)
5697 def get_and_connect_advanced_menuitem_menu(self, account):
5698 '''adds FOR ACCOUNT options'''
5699 xml = gtkgui_helpers.get_glade('advanced_menuitem_menu.glade')
5700 advanced_menuitem_menu = xml.get_widget('advanced_menuitem_menu')
5702 xml_console_menuitem = xml.get_widget('xml_console_menuitem')
5703 privacy_lists_menuitem = xml.get_widget('privacy_lists_menuitem')
5704 administrator_menuitem = xml.get_widget('administrator_menuitem')
5705 send_server_message_menuitem = xml.get_widget(
5706 'send_server_message_menuitem')
5707 set_motd_menuitem = xml.get_widget('set_motd_menuitem')
5708 update_motd_menuitem = xml.get_widget('update_motd_menuitem')
5709 delete_motd_menuitem = xml.get_widget('delete_motd_menuitem')
5711 xml_console_menuitem.connect('activate',
5712 self.on_xml_console_menuitem_activate, account)
5714 if gajim.connections[account] and gajim.connections[account].\
5715 privacy_rules_supported:
5716 privacy_lists_menuitem.connect('activate',
5717 self.on_privacy_lists_menuitem_activate, account)
5718 else:
5719 privacy_lists_menuitem.set_sensitive(False)
5721 if gajim.connections[account].is_zeroconf:
5722 administrator_menuitem.set_sensitive(False)
5723 send_server_message_menuitem.set_sensitive(False)
5724 set_motd_menuitem.set_sensitive(False)
5725 update_motd_menuitem.set_sensitive(False)
5726 delete_motd_menuitem.set_sensitive(False)
5727 else:
5728 send_server_message_menuitem.connect('activate',
5729 self.on_send_server_message_menuitem_activate, account)
5731 set_motd_menuitem.connect('activate',
5732 self.on_set_motd_menuitem_activate, account)
5734 update_motd_menuitem.connect('activate',
5735 self.on_update_motd_menuitem_activate, account)
5737 delete_motd_menuitem.connect('activate',
5738 self.on_delete_motd_menuitem_activate, account)
5740 advanced_menuitem_menu.show_all()
5742 return advanced_menuitem_menu
5744 def add_history_manager_menuitem(self, menu):
5745 '''adds a seperator and History Manager menuitem BELOW for account
5746 menuitems'''
5747 item = gtk.SeparatorMenuItem() # separator
5748 menu.append(item)
5750 # History manager
5751 item = gtk.ImageMenuItem(_('History Manager'))
5752 icon = gtk.image_new_from_stock(gtk.STOCK_JUSTIFY_FILL,
5753 gtk.ICON_SIZE_MENU)
5754 item.set_image(icon)
5755 menu.append(item)
5756 item.connect('activate', self.on_history_manager_menuitem_activate)
5758 def add_bookmarks_list(self, gc_sub_menu, account):
5759 '''Show join new group chat item and bookmarks list for an account'''
5760 item = gtk.ImageMenuItem(_('_Join New Group Chat'))
5761 icon = gtk.image_new_from_stock(gtk.STOCK_NEW, gtk.ICON_SIZE_MENU)
5762 item.set_image(icon)
5763 item.connect('activate', self.on_join_gc_activate, account)
5764 gc_sub_menu.append(item)
5766 # user has at least one bookmark
5767 if len(gajim.connections[account].bookmarks) > 0:
5768 item = gtk.SeparatorMenuItem() # separator
5769 gc_sub_menu.append(item)
5771 for bookmark in gajim.connections[account].bookmarks:
5772 item = gtk.MenuItem(bookmark['name'], False) # Do not use underline
5773 item.connect('activate', self.on_bookmark_menuitem_activate,
5774 account, bookmark)
5775 gc_sub_menu.append(item)
5777 def set_actions_menu_needs_rebuild(self):
5778 self.actions_menu_needs_rebuild = True
5780 def show_appropriate_context_menu(self, event, iters):
5781 # iters must be all of the same type
5782 model = self.modelfilter
5783 type_ = model[iters[0]][C_TYPE]
5784 for titer in iters[1:]:
5785 if model[titer][C_TYPE] != type_:
5786 return
5787 if type_ == 'group' and len(iters) == 1:
5788 self.make_group_menu(event, iters[0])
5789 if type_ == 'groupchat' and len(iters) == 1:
5790 self.make_groupchat_menu(event, iters[0])
5791 elif type_ == 'agent' and len(iters) == 1:
5792 self.make_transport_menu(event, iters[0])
5793 elif type_ in ('contact', 'self_contact') and len(iters) == 1:
5794 self.make_contact_menu(event, iters[0])
5795 elif type_ == 'contact':
5796 self.make_multiple_contact_menu(event, iters)
5797 elif type_ == 'account' and len(iters) == 1:
5798 self.make_account_menu(event, iters[0])
5800 def show_treeview_menu(self, event):
5801 try:
5802 model, list_of_paths = self.tree.get_selection().get_selected_rows()
5803 except TypeError:
5804 self.tree.get_selection().unselect_all()
5805 return
5806 if not len(list_of_paths):
5807 # no row is selected
5808 return
5809 if len(list_of_paths) > 1:
5810 iters = []
5811 for path in list_of_paths:
5812 iters.append(model.get_iter(path))
5813 else:
5814 path = list_of_paths[0]
5815 iters = [model.get_iter(path)]
5816 self.show_appropriate_context_menu(event, iters)
5818 return True
5820 ################################################################################
5822 ################################################################################
5824 def __init__(self):
5825 self.filtering = False
5826 self.xml = gtkgui_helpers.get_glade('roster_window.glade')
5827 self.window = self.xml.get_widget('roster_window')
5828 self.hpaned = self.xml.get_widget('roster_hpaned')
5829 gajim.interface.msg_win_mgr = MessageWindowMgr(self.window, self.hpaned)
5830 gajim.interface.msg_win_mgr.connect('window-delete',
5831 self.on_message_window_delete)
5832 self.advanced_menus = [] # We keep them to destroy them
5833 if gajim.config.get('roster_window_skip_taskbar'):
5834 self.window.set_property('skip-taskbar-hint', True)
5835 self.tree = self.xml.get_widget('roster_treeview')
5836 sel = self.tree.get_selection()
5837 sel.set_mode(gtk.SELECTION_MULTIPLE)
5838 #sel.connect('changed',
5839 # self.on_treeview_selection_changed)
5841 self._last_selected_contact = [] # holds a list of (jid, account) tupples
5842 self.transports_state_images = {'16': {}, '32': {}, 'opened': {},
5843 'closed': {}}
5845 self.last_save_dir = None
5846 self.editing_path = None # path of row with cell in edit mode
5847 self.add_new_contact_handler_id = False
5848 self.service_disco_handler_id = False
5849 self.new_chat_menuitem_handler_id = False
5850 self.single_message_menuitem_handler_id = False
5851 self.profile_avatar_menuitem_handler_id = False
5852 self.actions_menu_needs_rebuild = True
5853 self.regroup = gajim.config.get('mergeaccounts')
5854 self.clicked_path = None # Used remember on wich row we clicked
5855 if len(gajim.connections) < 2: # Do not merge accounts if only one exists
5856 self.regroup = False
5857 #FIXME: When list_accel_closures will be wrapped in pygtk
5858 # no need of this variable
5859 self.have_new_chat_accel = False # Is the "Ctrl+N" shown ?
5860 gtkgui_helpers.resize_window(self.window,
5861 gajim.config.get('roster_width'),
5862 gajim.config.get('roster_height'))
5863 gtkgui_helpers.move_window(self.window,
5864 gajim.config.get('roster_x-position'),
5865 gajim.config.get('roster_y-position'))
5867 self.popups_notification_height = 0
5868 self.popup_notification_windows = []
5870 # Remove contact from roster when last event opened
5871 # { (contact, account): { backend: boolean }
5872 self.contacts_to_be_removed = {}
5873 gajim.events.event_removed_subscribe(self.on_event_removed)
5875 # when this value become 0 we quit main application. If it's more than 0
5876 # it means we are waiting for this number of accounts to disconnect before
5877 # quitting
5878 self.quit_on_next_offline = -1
5880 # uf_show, img, show, sensitive
5881 liststore = gtk.ListStore(str, gtk.Image, str, bool)
5882 self.status_combobox = self.xml.get_widget('status_combobox')
5884 cell = cell_renderer_image.CellRendererImage(0, 1)
5885 self.status_combobox.pack_start(cell, False)
5887 # img to show is in in 2nd column of liststore
5888 self.status_combobox.add_attribute(cell, 'image', 1)
5889 # if it will be sensitive or not it is in the fourth column
5890 # all items in the 'row' must have sensitive to False
5891 # if we want False (so we add it for img_cell too)
5892 self.status_combobox.add_attribute(cell, 'sensitive', 3)
5894 cell = gtk.CellRendererText()
5895 cell.set_property('xpad', 5) # padding for status text
5896 self.status_combobox.pack_start(cell, True)
5897 # text to show is in in first column of liststore
5898 self.status_combobox.add_attribute(cell, 'text', 0)
5899 # if it will be sensitive or not it is in the fourth column
5900 self.status_combobox.add_attribute(cell, 'sensitive', 3)
5902 self.status_combobox.set_row_separator_func(self._iter_is_separator)
5904 for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'):
5905 uf_show = helpers.get_uf_show(show)
5906 liststore.append([uf_show, gajim.interface.jabber_state_images['16'][
5907 show], show, True])
5908 # Add a Separator (self._iter_is_separator() checks on string SEPARATOR)
5909 liststore.append(['SEPARATOR', None, '', True])
5911 path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'kbd_input.png')
5912 img = gtk.Image()
5913 img.set_from_file(path)
5914 # sensitivity to False because by default we're offline
5915 self.status_message_menuitem_iter = liststore.append(
5916 [_('Change Status Message...'), img, '', False])
5917 # Add a Separator (self._iter_is_separator() checks on string SEPARATOR)
5918 liststore.append(['SEPARATOR', None, '', True])
5920 uf_show = helpers.get_uf_show('offline')
5921 liststore.append([uf_show, gajim.interface.jabber_state_images['16'][
5922 'offline'], 'offline', True])
5924 status_combobox_items = ['online', 'chat', 'away', 'xa', 'dnd',
5925 'invisible', 'separator1', 'change_status_msg', 'separator2',
5926 'offline']
5927 self.status_combobox.set_model(liststore)
5929 # default to offline
5930 number_of_menuitem = status_combobox_items.index('offline')
5931 self.status_combobox.set_active(number_of_menuitem)
5933 # holds index to previously selected item so if "change status message..."
5934 # is selected we can fallback to previously selected item and not stay
5935 # with that item selected
5936 self.previous_status_combobox_active = number_of_menuitem
5938 showOffline = gajim.config.get('showoffline')
5939 showOnlyChatAndOnline = gajim.config.get('show_only_chat_and_online')
5941 w = self.xml.get_widget('show_offline_contacts_menuitem')
5942 w.set_active(showOffline)
5943 if showOnlyChatAndOnline:
5944 w.set_sensitive(False)
5946 w = self.xml.get_widget('show_only_active_contacts_menuitem')
5947 w.set_active(showOnlyChatAndOnline)
5948 if showOffline:
5949 w.set_sensitive(False)
5951 show_transports_group = gajim.config.get('show_transports_group')
5952 self.xml.get_widget('show_transports_menuitem').set_active(
5953 show_transports_group)
5955 self.xml.get_widget('show_roster_menuitem').set_active(True)
5957 # columns
5959 # this col has 3 cells:
5960 # first one img, second one text, third is sec pixbuf
5961 col = gtk.TreeViewColumn()
5963 def add_avatar_renderer():
5964 render_pixbuf = gtk.CellRendererPixbuf() # avatar img
5965 col.pack_start(render_pixbuf, expand=False)
5966 col.add_attribute(render_pixbuf, 'pixbuf',
5967 C_AVATAR_PIXBUF)
5968 col.set_cell_data_func(render_pixbuf,
5969 self._fill_avatar_pixbuf_renderer, None)
5971 if gajim.config.get('avatar_position_in_roster') == 'left':
5972 add_avatar_renderer()
5974 render_image = cell_renderer_image.CellRendererImage(0, 0)
5975 # show img or +-
5976 col.pack_start(render_image, expand=False)
5977 col.add_attribute(render_image, 'image', C_IMG)
5978 col.set_cell_data_func(render_image, self._iconCellDataFunc, None)
5980 render_text = gtk.CellRendererText() # contact or group or account name
5981 render_text.set_property('ellipsize', pango.ELLIPSIZE_END)
5982 col.pack_start(render_text, expand=True)
5983 col.add_attribute(render_text, 'markup', C_NAME) # where we hold the name
5984 col.set_cell_data_func(render_text, self._nameCellDataFunc, None)
5986 render_pixbuf = gtk.CellRendererPixbuf()
5987 col.pack_start(render_pixbuf, expand=False)
5988 col.add_attribute(render_pixbuf, 'pixbuf', C_MOOD_PIXBUF)
5989 col.set_cell_data_func(render_pixbuf,
5990 self._fill_mood_pixbuf_renderer, None)
5992 render_pixbuf = gtk.CellRendererPixbuf()
5993 col.pack_start(render_pixbuf, expand=False)
5994 col.add_attribute(render_pixbuf, 'pixbuf', C_ACTIVITY_PIXBUF)
5995 col.set_cell_data_func(render_pixbuf,
5996 self._fill_activity_pixbuf_renderer, None)
5998 render_pixbuf = gtk.CellRendererPixbuf()
5999 col.pack_start(render_pixbuf, expand=False)
6000 col.add_attribute(render_pixbuf, 'pixbuf', C_TUNE_PIXBUF)
6001 col.set_cell_data_func(render_pixbuf,
6002 self._fill_tune_pixbuf_renderer, None)
6004 if gajim.config.get('avatar_position_in_roster') == 'right':
6005 add_avatar_renderer()
6007 render_pixbuf = gtk.CellRendererPixbuf() # tls/ssl img
6008 col.pack_start(render_pixbuf, expand=False)
6009 col.add_attribute(render_pixbuf, 'pixbuf', C_PADLOCK_PIXBUF)
6010 col.set_cell_data_func(render_pixbuf,
6011 self._fill_padlock_pixbuf_renderer, None)
6012 self.tree.append_column(col)
6014 # do not show gtk arrows workaround
6015 col = gtk.TreeViewColumn()
6016 render_pixbuf = gtk.CellRendererPixbuf()
6017 col.pack_start(render_pixbuf, expand=False)
6018 self.tree.append_column(col)
6019 col.set_visible(False)
6020 self.tree.set_expander_column(col)
6022 # set search function
6023 self.tree.set_search_equal_func(self._search_roster_func)
6025 # signals
6026 self.TARGET_TYPE_URI_LIST = 80
6027 TARGETS = [('MY_TREE_MODEL_ROW',
6028 gtk.TARGET_SAME_APP | gtk.TARGET_SAME_WIDGET, 0)]
6029 TARGETS2 = [('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_WIDGET, 0),
6030 ('text/uri-list', 0, self.TARGET_TYPE_URI_LIST)]
6031 self.tree.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, TARGETS,
6032 gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE | gtk.gdk.ACTION_COPY)
6033 self.tree.enable_model_drag_dest(TARGETS2, gtk.gdk.ACTION_DEFAULT)
6034 self.tree.connect('drag_begin', self.drag_begin)
6035 self.tree.connect('drag_end', self.drag_end)
6036 self.tree.connect('drag_drop', self.drag_drop)
6037 self.tree.connect('drag_data_get', self.drag_data_get_data)
6038 self.tree.connect('drag_data_received', self.drag_data_received_data)
6039 self.dragging = False
6040 self.xml.signal_autoconnect(self)
6041 self.combobox_callback_active = True
6043 self.collapsed_rows = gajim.config.get('collapsed_rows').split('\t')
6044 self.tooltip = tooltips.RosterTooltip()
6045 # Workaroung: For strange reasons signal is behaving like row-changed
6046 self._toggeling_row = False
6047 self.setup_and_draw_roster()
6049 if gajim.config.get('show_roster_on_startup'):
6050 self.window.show_all()
6051 else:
6052 if not gajim.config.get('trayicon') or not \
6053 gajim.interface.systray_capabilities:
6054 # cannot happen via GUI, but I put this incase user touches
6055 # config. without trayicon, he or she should see the roster!
6056 self.window.show_all()
6057 gajim.config.set('show_roster_on_startup', True)
6059 if len(gajim.connections) == 0: # if we have no account
6060 def _open_wizard():
6061 gajim.interface.instances['account_creation_wizard'] = \
6062 config.AccountCreationWizardWindow()
6063 # Open wizard only after roster is created, so we can make it transient
6064 # for the roster window
6065 gobject.idle_add(_open_wizard)
6066 if not gajim.ZEROCONF_ACC_NAME in gajim.config.get_per('accounts'):
6067 # Create zeroconf in config file
6068 from common.zeroconf import connection_zeroconf
6069 connection_zeroconf.ConnectionZeroconf(gajim.ZEROCONF_ACC_NAME)
6071 # vim: se ts=3: