roster_window.py 229 KB
Newer Older
1
# -*- coding: utf-8 -*-
roidelapluie's avatar
roidelapluie committed
2
## src/roster_window.py
3
##
roidelapluie's avatar
roidelapluie committed
4 5
## Copyright (C) 2003-2008 Yann Leboulanger <asterix AT lagaule.org>
## Copyright (C) 2005 Alex Mauer <hawke AT hawkesnest.net>
roidelapluie's avatar
roidelapluie committed
6
##                    Stéphan Kochen <stephan AT kochen.nl>
roidelapluie's avatar
roidelapluie committed
7 8 9 10 11 12 13 14 15 16 17 18
## Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
## Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
##                         Nikos Kouremenos <kourem AT gmail.com>
## Copyright (C) 2006 Stefan Bethge <stefan AT lanpartei.de>
## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
## Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
##                    James Newton <redshodan AT gmail.com>
##                    Tomasz Melcer <liori AT exroot.org>
##                    Julien Pivotto <roidelapluie AT gmail.com>
## Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
##                    Jonathan Schleifer <js-gajim AT webkeks.org>
19
##
Yann Leboulanger's avatar
Yann Leboulanger committed
20 21 22
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
23
## it under the terms of the GNU General Public License as published
Yann Leboulanger's avatar
Yann Leboulanger committed
24
## by the Free Software Foundation; version 3 only.
25
##
Yann Leboulanger's avatar
Yann Leboulanger committed
26
## Gajim is distributed in the hope that it will be useful,
27
## but WITHOUT ANY WARRANTY; without even the implied warranty of
roidelapluie's avatar
roidelapluie committed
28
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 30
## GNU General Public License for more details.
##
Yann Leboulanger's avatar
Yann Leboulanger committed
31
## You should have received a copy of the GNU General Public License
roidelapluie's avatar
roidelapluie committed
32
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
Yann Leboulanger's avatar
Yann Leboulanger committed
33
##
34 35

import gtk
36
import pango
37 38
import gobject
import os
Yann Leboulanger's avatar
Yann Leboulanger committed
39
import sys
40 41
import time

42
import common.sleepy
43 44
import history_window
import dialogs
nkour's avatar
nkour committed
45
import vcard
46
import config
47
import disco
dkirov's avatar
dkirov committed
48
import gtkgui_helpers
49
import cell_renderer_image
50
import tooltips
nicfit's avatar
nicfit committed
51
import message_control
Liorithiel's avatar
Liorithiel committed
52
import adhoc_commands
53
import features_window
54 55 56

from common import gajim
from common import helpers
57 58
from common import passwords
from common.exceptions import GajimGeneralException
59
from common import i18n
60
from common import pep
61

62
from message_window import MessageWindowMgr
63

64 65
from session import ChatControlSession

66 67 68
from common import dbus_support
if dbus_support.supported:
	from music_track_listener import MusicTrackListener
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
69
	import dbus
Yann Leboulanger's avatar
Yann Leboulanger committed
70

71
from common.xmpp.protocol import NS_COMMANDS, NS_FILE, NS_MUC
js's avatar
js committed
72
from common.pep import MOODS, ACTIVITIES
73

js's avatar
js committed
74
try:
Yann Leboulanger's avatar
Yann Leboulanger committed
75
	from osx import syncmenu
js's avatar
js committed
76
except ImportError:
js's avatar
js committed
77
	pass
Yann Leboulanger's avatar
Yann Leboulanger committed
78

79
#(icon, name, type, jid, account, editable, second pixbuf)
80 81 82 83
(
C_IMG, # image to show state (online, new message etc)
C_NAME, # cellrenderer text that holds contact nickame
C_TYPE, # account, group or contact?
nkour's avatar
nkour committed
84
C_JID, # the jid of the row
85
C_ACCOUNT, # cellrenderer text that holds account name
js's avatar
js committed
86
C_MOOD_PIXBUF,
js's avatar
js committed
87
C_ACTIVITY_PIXBUF,
js's avatar
js committed
88
C_TUNE_PIXBUF,
89 90
C_AVATAR_PIXBUF, # avatar_pixbuf
C_PADLOCK_PIXBUF, # use for account row only
js's avatar
js committed
91
) = range(10)
92

93
class RosterWindow:
94
	'''Class for main window of the GTK+ interface'''
95

Yann Leboulanger's avatar
Yann Leboulanger committed
96
	def _get_account_iter(self, name, model=None):
js's avatar
js committed
97 98 99
		'''
		Return the gtk.TreeIter of the given account or None
		if not found.
100

Yann Leboulanger's avatar
Yann Leboulanger committed
101 102 103
		Keyword arguments:
		name -- the account name
		model -- the data model (default TreeFilterModel)
104 105
		'''
		if not model:
jimpp's avatar
jimpp committed
106
			model = self.modelfilter
nkour's avatar
nkour committed
107 108
		if model is None:
			return
109
		account_iter = model.get_iter_root()
110 111
		if self.regroup:
			return account_iter
112
		while account_iter:
113 114
			account_name = model[account_iter][C_ACCOUNT]
			if account_name and name == account_name.decode('utf-8'):
115
				break
116 117
			account_iter = model.iter_next(account_iter)
		return account_iter
118

119

Yann Leboulanger's avatar
Yann Leboulanger committed
120
	def _get_group_iter(self, name, account, account_iter=None, model=None):
js's avatar
js committed
121 122
		'''
		Return the gtk.TreeIter of the given group or None if not found.
123

Yann Leboulanger's avatar
Yann Leboulanger committed
124 125
		Keyword arguments:
		name -- the group name
126
		account -- the account name
127
		account_iter -- the iter of the account the model (default None)
Yann Leboulanger's avatar
Yann Leboulanger committed
128
		model -- the data model (default TreeFilterModel)
Yann Leboulanger's avatar
Yann Leboulanger committed
129

130 131
		'''
		if not model:
jimpp's avatar
jimpp committed
132
			model = self.modelfilter
133
		if not account_iter:
steve-e's avatar
steve-e committed
134
			account_iter = self._get_account_iter(account, model)
135
		group_iter = model.iter_children(account_iter)
136
		# C_NAME column contacts the pango escaped group name
137
		while group_iter:
138
			group_name = model[group_iter][C_JID].decode('utf-8')
139 140
			if name == group_name:
				break
141 142
			group_iter = model.iter_next(group_iter)
		return group_iter
Yann Leboulanger's avatar
Yann Leboulanger committed
143

144

Yann Leboulanger's avatar
Yann Leboulanger committed
145
	def _get_self_contact_iter(self, jid, account, model=None):
146 147
		''' Return the gtk.TreeIter of SelfContact or None if not found.

Yann Leboulanger's avatar
Yann Leboulanger committed
148
		Keyword arguments:
149
		jid -- the jid of SelfContact
Yann Leboulanger's avatar
Yann Leboulanger committed
150 151
		account -- the account of SelfContact
		model -- the data model (default TreeFilterModel)
Yann Leboulanger's avatar
Yann Leboulanger committed
152

153
		'''
Yann Leboulanger's avatar
Yann Leboulanger committed
154

155
		if not model:
jimpp's avatar
jimpp committed
156
			model = self.modelfilter
157
		iterAcct = self._get_account_iter(account, model)
158
		iterC = model.iter_children(iterAcct)
Yann Leboulanger's avatar
Yann Leboulanger committed
159

160 161 162 163 164 165 166 167
		# There might be several SelfContacts in merged account view
		while iterC:
			if model[iterC][C_TYPE] != 'self_contact':
				break
			iter_jid = model[iterC][C_JID]
			if iter_jid and jid == iter_jid.decode('utf-8'):
				return iterC
			iterC = model.iter_next(iterC)
168 169
		return None

170

steve-e's avatar
steve-e committed
171
	def _get_contact_iter(self, jid, account, contact=None, model=None):
172
		''' Return a list of gtk.TreeIter of the given contact.
173

174
		Keyword arguments:
175
		jid -- the jid without resource
176
		account -- the account
177
		contact -- the contact (default None)
178
		model -- the data model (default TreeFilterModel)
Yann Leboulanger's avatar
Yann Leboulanger committed
179

180 181
		'''
		if not model:
jimpp's avatar
jimpp committed
182
			model = self.modelfilter
js's avatar
js committed
183 184
			# when closing Gajim model can be none (async pbs?)
			if model is None:
185 186
				return []

187
		if jid == gajim.get_jid_from_account(account):
steve-e's avatar
steve-e committed
188
			contact_iter = self._get_self_contact_iter(jid,	account, model)
189 190
			if contact_iter:
				return [contact_iter]
191 192
			else:
				return []
193 194

		if not contact:
steve-e's avatar
steve-e committed
195
			contact = gajim.contacts.get_first_contact_from_jid(account, jid)
196 197
			if not contact:
				# We don't know this contact
steve-e's avatar
steve-e committed
198
				return []
199

200
		acct = self._get_account_iter(account, model)
201
		found = [] # the contact iters. One per group
jimpp's avatar
jimpp committed
202
		for group in contact.get_shown_groups():
steve-e's avatar
steve-e committed
203
			group_iter = self._get_group_iter(group, account, acct,  model)
204
			contact_iter = model.iter_children(group_iter)
205

206
			while contact_iter:
207
				# Loop over all contacts in this group
208
				iter_jid = model[contact_iter][C_JID]
steve-e's avatar
steve-e committed
209 210
				if iter_jid and jid == iter_jid.decode('utf-8') and \
				account == model[contact_iter][C_ACCOUNT].decode('utf-8'):
211
					# only one iter per group
212
					found.append(contact_iter)
213 214 215
					contact_iter = None
				elif model.iter_has_child(contact_iter):
					# it's a big brother and has children
steve-e's avatar
steve-e committed
216
					contact_iter = model.iter_children(contact_iter)
Yann Leboulanger's avatar
Yann Leboulanger committed
217
				else:
218
					# try to find next contact:
js's avatar
js committed
219 220
					# other contact in this group or
					# brother contact
steve-e's avatar
steve-e committed
221
					next_contact_iter = model.iter_next(contact_iter)
222 223 224
					if next_contact_iter:
						contact_iter = next_contact_iter
					else:
js's avatar
js committed
225 226
						# It's the last one.
						# Go up if we are big brother
steve-e's avatar
steve-e committed
227 228 229
						parent_iter = model.iter_parent(contact_iter)
						if parent_iter and model[parent_iter][C_TYPE] == 'contact':
							contact_iter = model.iter_next(parent_iter)
230
						else:
js's avatar
js committed
231
							# we tested all
js's avatar
js committed
232
							# contacts in this group
233
							contact_iter = None
234
		return found
Yann Leboulanger's avatar
Yann Leboulanger committed
235 236


237
	def _iter_is_separator(self, model, titer):
238
		''' Return True if the given iter is a separator.
Yann Leboulanger's avatar
Yann Leboulanger committed
239

240
		Keyword arguments:
241
		model -- the data model
Yann Leboulanger's avatar
Yann Leboulanger committed
242
		iter -- the gtk.TreeIter to test
243
		'''
244
		if model[titer][0] == 'SEPARATOR':
245 246
			return True
		return False
247

248

Yann Leboulanger's avatar
Yann Leboulanger committed
249
	def _iter_contact_rows(self, model=None):
250
		'''Iterate over all contact rows in given model.
Yann Leboulanger's avatar
Yann Leboulanger committed
251

252 253
		Keyword argument
		model -- the data model (default TreeFilterModel)
254 255
		'''
		if not model:
jimpp's avatar
jimpp committed
256
			model = self.modelfilter
257 258 259 260 261 262 263
		account_iter = model.get_iter_root()
		while account_iter:
			group_iter = model.iter_children(account_iter)
			while group_iter:
				contact_iter = model.iter_children(group_iter)
				while contact_iter:
					yield model[contact_iter]
js's avatar
js committed
264 265
					contact_iter = model.iter_next(
						contact_iter)
266 267
				group_iter = model.iter_next(group_iter)
			account_iter = model.iter_next(account_iter)
Yann Leboulanger's avatar
Yann Leboulanger committed
268 269 270


#############################################################################
271
### Methods for adding and removing roster window items
steve-e's avatar
steve-e committed
272
#############################################################################
Yann Leboulanger's avatar
Yann Leboulanger committed
273

274
	def add_account(self, account):
js's avatar
js committed
275 276 277 278
		'''
		Add account to roster and draw it. Do nothing if it is
		already in.
		'''
279
		if self._get_account_iter(account):
Yann Leboulanger's avatar
Yann Leboulanger committed
280
			# Will happen on reconnect or for merged accounts
281
			return
Yann Leboulanger's avatar
Yann Leboulanger committed
282

283
		if self.regroup:
284
			# Merged accounts view
285
			show = helpers.get_global_show()
js's avatar
js committed
286 287 288
			self.model.append(None, [
				gajim.interface.jabber_state_images['16'][show],
				_('Merged accounts'), 'account', '', 'all',
js's avatar
js committed
289
				None, None, None, None, None])
290
		else:
steve-e's avatar
steve-e committed
291
			show = gajim.SHOW_LIST[gajim.connections[account].connected]
292
			our_jid = gajim.get_jid_from_account(account)
293

294 295
			tls_pixbuf = None
			if gajim.account_is_securely_connected(account):
Yann Leboulanger's avatar
Yann Leboulanger committed
296 297
				# the only way to create a pixbuf from stock
				tls_pixbuf = self.window.render_icon(
js's avatar
js committed
298 299
					gtk.STOCK_DIALOG_AUTHENTICATION,
					gtk.ICON_SIZE_MENU)
300

js's avatar
js committed
301 302 303
			self.model.append(None, [
				gajim.interface.jabber_state_images['16'][show],
				gobject.markup_escape_text(account), 'account',
js's avatar
js committed
304 305
				our_jid, account, None, None, None, None,
				tls_pixbuf])
306

jimpp's avatar
jimpp committed
307
		self.draw_account(account)
308

309

310
	def add_account_contacts(self, account):
jimpp's avatar
jimpp committed
311 312
		'''Add all contacts and groups of the given account to roster,
		draw them and account.
313
		'''
jimpp's avatar
jimpp committed
314
		self.starting = True
315
		jids = gajim.contacts.get_jid_list(account)
Yann Leboulanger's avatar
Yann Leboulanger committed
316

317 318 319 320
		self.tree.freeze_child_notify()
		for jid in jids:
			self.add_contact(jid, account)
		self.tree.thaw_child_notify()
steve-e's avatar
steve-e committed
321 322 323 324 325

		# Do not freeze the GUI when drawing the contacts
		if jids:
			# Overhead is big, only invoke when needed
			self._idle_draw_jids_of_account(jids, account)
326 327

		# Draw all known groups
steve-e's avatar
steve-e committed
328
		for group in gajim.groups[account]:
329 330
			self.draw_group(group, account)
		self.draw_account(account)
jimpp's avatar
jimpp committed
331
		self.starting = False
332

Yann Leboulanger's avatar
Yann Leboulanger committed
333

Yann Leboulanger's avatar
Yann Leboulanger committed
334 335
	def _add_entity(self, contact, account, groups=None,
	big_brother_contact=None, big_brother_account=None):
336
		'''Add the given contact to roster data model.
Yann Leboulanger's avatar
Yann Leboulanger committed
337

338 339
		Contact is added regardless if he is already in roster or not.
		Return list of newly added iters.
Yann Leboulanger's avatar
Yann Leboulanger committed
340

341 342
		Keyword arguments:
		contact -- the contact to add
343
		account -- the contacts account
js's avatar
js committed
344 345
		groups -- list of groups to add the contact to.
			  (default groups in contact.get_shown_groups()).
346
			Parameter ignored when big_brother_contact is specified.
js's avatar
js committed
347 348
		big_brother_contact -- if specified contact is added as child
			  big_brother_contact. (default None)
349 350 351 352
		'''
		added_iters = []
		if big_brother_contact:
			# Add contact under big brother
353

js's avatar
js committed
354 355 356
			parent_iters = self._get_contact_iter(
				big_brother_contact.jid, big_brother_account,
				big_brother_contact, self.model)
Yann Leboulanger's avatar
Yann Leboulanger committed
357
			assert len(parent_iters) > 0, 'Big brother is not yet in roster!'
358

steve-e's avatar
steve-e committed
359 360
			# Do not confuse get_contact_iter: Sync groups of family members
			contact.groups = big_brother_contact.get_shown_groups()[:]
361

362
			for child_iter in parent_iters:
steve-e's avatar
steve-e committed
363 364
				it = self.model.append(child_iter, (None,	contact.get_shown_name(), 
				'contact', contact.jid, account, None, None, None, None, None))
365 366 367 368
				added_iters.append(it)
		else:
			# We are a normal contact. Add us to our groups.
			if not groups:
jimpp's avatar
jimpp committed
369
				groups = contact.get_shown_groups()
370
			for group in groups:
steve-e's avatar
steve-e committed
371 372
				child_iterG = self._get_group_iter(group, account, 
						model = self.model)
373 374
				if not child_iterG:
					# Group is not yet in roster, add it!
steve-e's avatar
steve-e committed
375 376 377 378 379
					child_iterA = self._get_account_iter(account, self.model)
					child_iterG = self.model.append(child_iterA, 
						[gajim.interface.jabber_state_images['16']['closed'], 
						gobject.markup_escape_text(group),
						'group', group, account, None, None, None, None, None])
jimpp's avatar
jimpp committed
380
					self.draw_group(group, account)
381 382 383 384 385 386 387

				if contact.is_transport():
					typestr = 'agent'
				elif contact.is_groupchat():
					typestr = 'groupchat'
				else:
					typestr = 'contact'
Yann Leboulanger's avatar
Yann Leboulanger committed
388

js's avatar
js committed
389 390 391 392
				# we add some values here. see draw_contact
				# for more
				i_ = self.model.append(child_iterG, (None,
					contact.get_shown_name(), typestr,
js's avatar
js committed
393
					contact.jid, account, None, None, None,
js's avatar
js committed
394
					None, None))
395
				added_iters.append(i_)
steve-e's avatar
steve-e committed
396 397

				# Restore the group expand state
398 399 400 401
				if account + group in self.collapsed_rows:
					is_expanded = False
				else:
					is_expanded = True
steve-e's avatar
steve-e committed
402
				if group not in gajim.groups[account]:
steve-e's avatar
steve-e committed
403 404 405
					gajim.groups[account][group] = {'expand': is_expanded}

		assert len(added_iters), '%s has not been added to roster!' % contact.jid
Yann Leboulanger's avatar
Yann Leboulanger committed
406 407
		return added_iters

Yann Leboulanger's avatar
Yann Leboulanger committed
408
	def _remove_entity(self, contact, account, groups=None):
409
		'''Remove the given contact from roster data model.
Yann Leboulanger's avatar
Yann Leboulanger committed
410

411
		Empty groups after contact removal are removed too.
js's avatar
js committed
412 413
		Return False if contact still has children and deletion was
		not performed.
414
		Return True on success.
Yann Leboulanger's avatar
Yann Leboulanger committed
415

416 417
		Keyword arguments:
		contact -- the contact to add
418
		account -- the contacts account
jimpp's avatar
jimpp committed
419
		groups -- list of groups to remove the contact from.
420
		'''
steve-e's avatar
steve-e committed
421 422
		iters = self._get_contact_iter(contact.jid, account, contact, self.model)
		assert iters, '%s shall be removed but is not in roster' % contact.jid
423 424 425 426 427 428 429

		parent_iter = self.model.iter_parent(iters[0])
		parent_type = self.model[parent_iter][C_TYPE]

		if groups:
			# Only remove from specified groups
			all_iters = iters[:]
steve-e's avatar
steve-e committed
430
			group_iters = [self._get_group_iter(group, account) 
js's avatar
js committed
431
				for group in groups]
Yann Leboulanger's avatar
Yann Leboulanger committed
432 433
			iters = [titer for titer in all_iters
				if self.model.iter_parent(titer) in group_iters]
434 435 436 437 438 439

		iter_children = self.model.iter_children(iters[0])

		if iter_children:
			# We have children. We cannot be removed!
			return False
440
		else:
441 442
			# Remove us and empty groups from the model
			for i in iters:
443 444 445
				assert self.model[i][C_JID] == contact.jid and \
					self.model[i][C_ACCOUNT] == account, \
					"Invalidated iters of %s" % contact.jid
steve-e's avatar
steve-e committed
446

447
				parent_i = self.model.iter_parent(i)
steve-e's avatar
steve-e committed
448

449
				if parent_type == 'group' and \
Yann Leboulanger's avatar
Yann Leboulanger committed
450
				self.model.iter_n_children(parent_i) == 1:
steve-e's avatar
steve-e committed
451
					group = self.model[parent_i][C_JID].decode('utf-8')
452
					if group in gajim.groups[account]:
453 454
						del gajim.groups[account][group]
					self.model.remove(parent_i)
455 456
				else:
					self.model.remove(i)
457
			return True
458

steve-e's avatar
steve-e committed
459
	def _add_metacontact_family(self, family, account):
js's avatar
js committed
460 461
		'''
		Add the give Metacontact family to roster data model.
Yann Leboulanger's avatar
Yann Leboulanger committed
462 463

		Add Big Brother to his groups and all others under him.
js's avatar
js committed
464 465
		Return list of all added (contact, account) tuples with
		Big Brother as first element.
Yann Leboulanger's avatar
Yann Leboulanger committed
466

467 468 469
		Keyword arguments:
		family -- the family, see Contacts.get_metacontacts_family()
		'''
steve-e's avatar
steve-e committed
470

471 472
		nearby_family, big_brother_jid, big_brother_account = \
			self._get_nearby_family_and_big_brother(family, account)
steve-e's avatar
steve-e committed
473 474
		big_brother_contact = gajim.contacts.get_first_contact_from_jid(
			big_brother_account, big_brother_jid)
475

js's avatar
js committed
476
		assert len(self._get_contact_iter(big_brother_jid,
steve-e's avatar
steve-e committed
477 478
			big_brother_account, big_brother_contact, self.model)) == 0, \
			'Big brother %s already in roster\n Family: %s' \
js's avatar
js committed
479
			% (big_brother_jid, family)
480
		self._add_entity(big_brother_contact, big_brother_account)
Yann Leboulanger's avatar
Yann Leboulanger committed
481

482
		brothers = []
steve-e's avatar
steve-e committed
483 484
		# Filter family members
		for data in nearby_family:
485 486
			_account = data['account']
			_jid = data['jid']
js's avatar
js committed
487 488
			_contact = gajim.contacts.get_first_contact_from_jid(
				_account, _jid)
489

490
			if not _contact or _contact == big_brother_contact:
491
				# Corresponding account is not connected
492
				# or brother already added
493
				continue
Yann Leboulanger's avatar
Yann Leboulanger committed
494

js's avatar
js committed
495
			assert len(self._get_contact_iter(_jid, _account,
steve-e's avatar
steve-e committed
496 497
				_contact, self.model)) == 0, \
				"%s already in roster.\n Family: %s" % (_jid, nearby_family)
js's avatar
js committed
498 499 500
			self._add_entity(_contact, _account,
				big_brother_contact = big_brother_contact,
				big_brother_account = big_brother_account)
501 502 503 504 505
			brothers.append((_contact, _account))

		brothers.insert(0, (big_brother_contact, big_brother_account))
		return brothers

steve-e's avatar
steve-e committed
506
	def _remove_metacontact_family(self, family, account):
js's avatar
js committed
507 508
		'''
		Remove the given Metacontact family from roster data model.
Yann Leboulanger's avatar
Yann Leboulanger committed
509

js's avatar
js committed
510 511
		See Contacts.get_metacontacts_family() and
		RosterWindow._remove_entity()
512
		'''
513 514
		nearby_family = self._get_nearby_family_and_big_brother(
			family, account)[0]
steve-e's avatar
steve-e committed
515

Yann Leboulanger's avatar
Yann Leboulanger committed
516
		# Family might has changed (actual big brother not on top).
steve-e's avatar
steve-e committed
517 518
		# Remove childs first then big brother
		family_in_roster = False
519
		big_brother_jid = None
steve-e's avatar
steve-e committed
520
		for data in nearby_family:
521 522 523
			_account = data['account']
			_jid = data['jid']
			_contact = gajim.contacts.get_first_contact_from_jid(_account, _jid)
Yann Leboulanger's avatar
Yann Leboulanger committed
524

525
			iters = self._get_contact_iter(_jid, _account, _contact, self.model)
steve-e's avatar
steve-e committed
526 527 528 529
			if not iters or not _contact:
				# Family might not be up to date.
				# Only try to remove what is actually in the roster
				continue
Yann Leboulanger's avatar
Yann Leboulanger committed
530 531
			assert iters, '%s shall be removed but is not in roster \
				\n Family: %s' % (_jid, family)
steve-e's avatar
steve-e committed
532 533

			family_in_roster = True
534 535 536

			parent_iter = self.model.iter_parent(iters[0])
			parent_type = self.model[parent_iter][C_TYPE]
Yann Leboulanger's avatar
Yann Leboulanger committed
537

538 539 540 541 542 543 544 545
			if parent_type != 'contact':
				# The contact on top
				old_big_account = _account
				old_big_contact = _contact
				old_big_jid = _jid
				continue

			ok = self._remove_entity(_contact, _account)
Yann Leboulanger's avatar
Yann Leboulanger committed
546 547 548 549
			assert ok, '%s was not removed' % _jid
			assert len(self._get_contact_iter(_jid, _account, _contact,
				self.model)) == 0, '%s is removed but still in roster' % _jid

steve-e's avatar
steve-e committed
550 551
		if not family_in_roster:
			return False
552 553 554
	
		assert old_big_jid, 'No Big Brother in nearby family % (Family: %)' % \
			(nearby_family, family)
Yann Leboulanger's avatar
Yann Leboulanger committed
555 556 557 558
		iters = self._get_contact_iter(old_big_jid, old_big_account,
			old_big_contact, self.model)
		assert len(iters) > 0, 'Old Big Brother %s is not in roster anymore' % \
			old_big_jid
Yann Leboulanger's avatar
Yann Leboulanger committed
559
		assert not self.model.iter_children(iters[0]),\
Yann Leboulanger's avatar
Yann Leboulanger committed
560 561
			'Old Big Brother %s still has children' % old_big_jid

562 563
		ok = self._remove_entity(old_big_contact, old_big_account)
		assert ok, "Old Big Brother %s not removed" % old_big_jid
Yann Leboulanger's avatar
Yann Leboulanger committed
564
		assert len(self._get_contact_iter(old_big_jid, old_big_account,
Yann Leboulanger's avatar
Yann Leboulanger committed
565
			old_big_contact, self.model)) == 0,\
Yann Leboulanger's avatar
Yann Leboulanger committed
566 567
			'Old Big Brother %s is removed but still in roster' % old_big_jid

568
		return True
Yann Leboulanger's avatar
Yann Leboulanger committed
569

570 571 572 573 574

	def _recalibrate_metacontact_family(self, family, account):
		'''Regroup metacontact family if necessary.'''

		brothers = []
575 576
		nearby_family, big_brother_jid, big_brother_account = \
			self._get_nearby_family_and_big_brother(family, account)
steve-e's avatar
steve-e committed
577 578
		child_iters = self._get_contact_iter(big_brother_jid, big_brother_account,
			model = self.model)
579 580 581 582 583
		parent_iter = self.model.iter_parent(child_iters[0])
		parent_type = self.model[parent_iter][C_TYPE]

		# Check if the current BigBrother has even been before.
		if parent_type == 'contact':
584 585 586 587 588
			for data in nearby_family:
				# recalibrate after remove to keep highlight
				if data['jid'] in gajim.to_be_removed[data['account']]:
					return

589 590
			self._remove_metacontact_family(family, account)
			brothers = self._add_metacontact_family(family, account)
591
			
592
			for c, acc in brothers:
593
				self.draw_completely(c.jid, acc)
594 595


596 597 598 599
	def _get_nearby_family_and_big_brother(self, family, account):
		'''Return the nearby family and its Big Brother

		Nearby family is the part of the family that is grouped with the metacontact.
js's avatar
js committed
600
		A metacontact may be over different accounts. If regroup is s False the
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620
		given family is split account wise.

		(nearby_family, big_brother_jid, big_brother_account)
		'''
		if self.regroup:
			# group all together
			nearby_family = family
		else:
			# we want one nearby_family per account
			nearby_family = [data for data in family
				if account == data['account']]

		big_brother_data = gajim.contacts.get_metacontacts_big_brother(
			nearby_family)
		big_brother_jid = big_brother_data['jid']
		big_brother_account = big_brother_data['account']

		return (nearby_family, big_brother_jid, big_brother_account)


621 622
	def _add_self_contact(self, account):
		'''Add account's SelfContact to roster and draw it and the account.
Yann Leboulanger's avatar
Yann Leboulanger committed
623

624 625 626
		Return the SelfContact contact instance
		'''
		jid = gajim.get_jid_from_account(account)
627
		contact = gajim.contacts.get_first_contact_from_jid(account, jid)
Yann Leboulanger's avatar
Yann Leboulanger committed
628 629 630

		assert len(self._get_contact_iter(jid, account, contact, self.model)) == \
			0, 'Self contact %s already in roster' % jid
631 632 633

		child_iterA = self._get_account_iter(account, self.model)
		self.model.append(child_iterA, (None, gajim.nicks[account],
js's avatar
js committed
634 635
			'self_contact', jid, account, None, None, None, None,
			None))
Yann Leboulanger's avatar
Yann Leboulanger committed
636

637
		self.draw_completely(jid, account)
638 639 640
		self.draw_account(account)

		return contact
Yann Leboulanger's avatar
Yann Leboulanger committed
641 642


643
	def add_contact(self, jid, account):
jimpp's avatar
jimpp committed
644
		'''Add contact to roster and draw him.
Yann Leboulanger's avatar
Yann Leboulanger committed
645

jimpp's avatar
jimpp committed
646 647
		Add contact to all its group and redraw the groups, the contact and the
		account. If it's a Metacontact, add and draw the whole family.
Yann Leboulanger's avatar
Yann Leboulanger committed
648 649 650
		Do nothing if the contact is already in roster.

		Return the added contact instance. If it is a Metacontact return
651
		Big Brother.
Yann Leboulanger's avatar
Yann Leboulanger committed
652

653 654 655
		Keyword arguments:
		jid -- the contact's jid or SelfJid to add SelfContact
		account -- the corresponding account.
Yann Leboulanger's avatar
Yann Leboulanger committed
656

657
		'''
658 659
		contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
		if len(self._get_contact_iter(jid, account, contact, self.model)):
660
			# If contact already in roster, do nothing
661
			return
662

663
		if jid == gajim.get_jid_from_account(account):
Yann Leboulanger's avatar
Yann Leboulanger committed
664
			if contact.resource != gajim.connections[account].server_resource:
665
				return self._add_self_contact(account)
666
			return
Yann Leboulanger's avatar
Yann Leboulanger committed
667

steve-e's avatar
steve-e committed
668 669
		is_observer = contact.is_observer()
		if is_observer:
670 671 672 673
			# if he has a tag, remove it
			tag = gajim.contacts.get_metacontacts_tag(account, jid)
			if tag:
				gajim.contacts.remove_metacontact(account, jid)
Yann Leboulanger's avatar
Yann Leboulanger committed
674

675
		# Add contact to roster
676
		family = gajim.contacts.get_metacontacts_family(account, jid)
677
		contacts = []
678
		if family:
679
			# We have a family. So we are a metacontact.
steve-e's avatar
steve-e committed
680 681 682 683 684
			# Add all family members that we shall be grouped with
			if self.regroup:
				# remove existing family members to regroup them
				self._remove_metacontact_family(family, account)
			contacts = self._add_metacontact_family(family, account)
685 686 687 688
		else:
			# We are a normal contact
			contacts = [(contact, account),]
			self._add_entity(contact, account)
689

jimpp's avatar
jimpp committed
690 691 692
		# Draw the contact and its groups contact
		if not self.starting:
			for c, acc in contacts:
693
				self.draw_completely(c.jid, acc)
jimpp's avatar
jimpp committed
694
			for group in contact.get_shown_groups():
jimpp's avatar
jimpp committed
695
				self.draw_group(group, account)
696
				self._adjust_group_expand_collapse_state(group, account)
jimpp's avatar
jimpp committed
697 698
			self.draw_account(account)

699
		return contacts[0][0] # it's contact/big brother with highest priority
700

701
	def remove_contact(self, jid, account, force=False, backend=False):
702
		'''Remove contact from roster.
Yann Leboulanger's avatar
Yann Leboulanger committed
703 704 705

		Remove contact from all its group. Remove empty groups or redraw
		otherwise.
706 707
		Draw the account.
		If it's a Metacontact, remove the whole family.
Yann Leboulanger's avatar
Yann Leboulanger committed
708 709
		Do nothing if the contact is not in roster.

710 711 712
		Keyword arguments:
		jid -- the contact's jid or SelfJid to remove SelfContact
		account -- the corresponding account.
713 714
		force -- remove contact even it has pending evens (Default False)
		backend -- also remove contact instance (Default False)
Yann Leboulanger's avatar
Yann Leboulanger committed
715

716
		'''
717
		contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
718
		if not contact:
719
			return
720

721 722 723 724 725
		if not force and (self.contact_has_pending_roster_events(contact,
		account) or gajim.interface.msg_win_mgr.get_control(jid, account)):
			# Contact has pending events or window
			#TODO: or single message windows? Bur they are not listed for the
			# moment
726
			key = (jid, account)
727
			if not key in self.contacts_to_be_removed:
728
				self.contacts_to_be_removed[key] = {'backend': backend}
729 730 731 732 733 734
			# if more pending event, don't remove from roster
			if self.contact_has_pending_roster_events(contact, account):
				return False

		iters = self._get_contact_iter(jid, account, contact, self.model)
		if iters:
735 736 737 738 739 740
			# no more pending events
			# Remove contact from roster directly
			family = gajim.contacts.get_metacontacts_family(account, jid)
			if family:
				# We have a family. So we are a metacontact.
				self._remove_metacontact_family(family, account)
Yann Leboulanger's avatar
Yann Leboulanger committed
741
			else:
742
				self._remove_entity(contact, account)
743

744 745 746 747 748 749
		if backend and (not gajim.interface.msg_win_mgr.get_control(jid, account)\
		or force):
			# If a window is still opened: don't remove contact instance
			# Remove contact before redrawing, otherwise the old
			# numbers will still be show
			gajim.contacts.remove_jid(account, jid, remove_meta=True)
750 751 752
			rest_of_family = [data for data in family
				if account != data['account'] or jid != data['jid']]
			if iters and rest_of_family:
753
				# reshow the rest of the family
754
				brothers = self._add_metacontact_family(rest_of_family, account)
755
				for c, acc in brothers:
756
					self.draw_completely(c.jid, acc)
757 758

		if iters:
759
			# Draw all groups of the contact
jimpp's avatar
jimpp committed
760
			for group in contact.get_shown_groups():
761 762 763
				self.draw_group(group, account)
			self.draw_account(account)

764
		return True
765

Yann Leboulanger's avatar
Yann Leboulanger committed
766
	def add_groupchat(self, jid, account, status=''):
767 768 769
		'''Add groupchat to roster and draw it.
		Return the added contact instance.
		'''
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
770
		contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
steve-e's avatar
steve-e committed
771 772 773
		# Do not show gc if we are disconnected and minimize it
		if gajim.account_is_connected(account):
			show = 'online'
js's avatar
js committed
774
		else:
steve-e's avatar
steve-e committed
775 776 777
			show = 'offline'
			status = ''

778
		if contact is None:
steve-e's avatar
steve-e committed
779 780 781 782
			gc_control = gajim.interface.msg_win_mgr.get_gc_control(jid, account)
			if gc_control:
				# there is a window that we can minimize
				gajim.interface.minimized_controls[account][jid] = gc_control
783 784 785 786 787 788 789 790 791
				name = gc_control.name
			elif jid in gajim.interface.minimized_controls[account]:
				name = gajim.interface.minimized_controls[account][jid].name
			else:
				name = jid.split('@')[0]
			# New groupchat
			contact = gajim.contacts.create_contact(jid=jid, name=name,
				groups=[_('Groupchats')], show=show, status=status, sub='none')
			gajim.contacts.add_contact(account, contact)
steve-e's avatar
steve-e committed
792
			self.add_contact(jid, account)
793
		else:
steve-e's avatar
steve-e committed
794 795
			contact.show = show
			contact.status = status
796
			self.adjust_and_draw_contact_context(jid, account)
steve-e's avatar
steve-e committed
797

Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
798
		return contact
Yann Leboulanger's avatar
Yann Leboulanger committed
799

Piotr Gaczkowski's avatar
Piotr Gaczkowski committed