1# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org> 2# Copyright (C) 2005 Alex Mauer <hawke AT hawkesnest.net> 3# Stéphan Kochen <stephan AT kochen.nl> 4# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com> 5# Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com> 6# Nikos Kouremenos <kourem AT gmail.com> 7# Copyright (C) 2006 Stefan Bethge <stefan AT lanpartei.de> 8# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org> 9# Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net> 10# James Newton <redshodan AT gmail.com> 11# Tomasz Melcer <liori AT exroot.org> 12# Julien Pivotto <roidelapluie AT gmail.com> 13# Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de> 14# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com> 15# Jonathan Schleifer <js-gajim AT webkeks.org> 16# 17# This file is part of Gajim. 18# 19# Gajim is free software; you can redistribute it and/or modify 20# it under the terms of the GNU General Public License as published 21# by the Free Software Foundation; version 3 only. 22# 23# Gajim is distributed in the hope that it will be useful, 24# but WITHOUT ANY WARRANTY; without even the implied warranty of 25# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 26# GNU General Public License for more details. 27# 28# You should have received a copy of the GNU General Public License 29# along with Gajim. If not, see <http://www.gnu.org/licenses/>. 30 31import os 32import sys 33import time 34import locale 35import logging 36from enum import IntEnum, unique 37 38from gi.repository import Gtk 39from gi.repository import Gdk 40from gi.repository import Pango 41from gi.repository import GObject 42from gi.repository import GLib 43from gi.repository import Gio 44from nbxmpp.namespaces import Namespace 45 46from gajim import dialogs 47from gajim import vcard 48from gajim import gtkgui_helpers 49from gajim import gui_menu_builder 50 51from gajim.common import app 52from gajim.common import helpers 53from gajim.common.exceptions import GajimGeneralException 54from gajim.common import i18n 55from gajim.common.helpers import save_roster_position 56from gajim.common.helpers import ask_for_status_message 57from gajim.common.i18n import _ 58from gajim.common.const import PEPEventType, AvatarSize, StyleAttr 59from gajim.common.dbus import location 60 61from gajim.common import ged 62from gajim.message_window import MessageWindowMgr 63 64from gajim.gui.dialogs import DialogButton 65from gajim.gui.dialogs import ConfirmationDialog 66from gajim.gui.dialogs import ConfirmationCheckDialog 67from gajim.gui.dialogs import ErrorDialog 68from gajim.gui.dialogs import InputDialog 69from gajim.gui.dialogs import InformationDialog 70from gajim.gui.single_message import SingleMessageWindow 71from gajim.gui.add_contact import AddNewContactWindow 72from gajim.gui.service_registration import ServiceRegistration 73from gajim.gui.discovery import ServiceDiscoveryWindow 74from gajim.gui.tooltips import RosterTooltip 75from gajim.gui.adhoc import AdHocCommand 76from gajim.gui.status_selector import StatusSelector 77from gajim.gui.util import get_icon_name 78from gajim.gui.util import resize_window 79from gajim.gui.util import restore_roster_position 80from gajim.gui.util import get_metacontact_surface 81from gajim.gui.util import get_builder 82from gajim.gui.util import set_urgency_hint 83from gajim.gui.util import get_activity_icon_name 84from gajim.gui.util import get_account_activity_icon_name 85from gajim.gui.util import get_account_mood_icon_name 86from gajim.gui.util import get_account_tune_icon_name 87from gajim.gui.util import get_account_location_icon_name 88from gajim.gui.util import open_window 89 90 91log = logging.getLogger('gajim.roster') 92 93@unique 94class Column(IntEnum): 95 IMG = 0 # image to show state (online, new message etc) 96 NAME = 1 # cellrenderer text that holds contact nickname 97 TYPE = 2 # account, group or contact? 98 JID = 3 # the jid of the row 99 ACCOUNT = 4 # cellrenderer text that holds account name 100 MOOD_PIXBUF = 5 101 ACTIVITY_PIXBUF = 6 102 TUNE_ICON = 7 103 LOCATION_ICON = 8 104 AVATAR_IMG = 9 # avatar_sha 105 PADLOCK_PIXBUF = 10 # use for account row only 106 VISIBLE = 11 107 108 109class RosterWindow: 110 """ 111 Class for main window of the GTK interface 112 """ 113 114 def _get_account_iter(self, name, model=None): 115 """ 116 Return the Gtk.TreeIter of the given account or None if not found 117 118 Keyword arguments: 119 name -- the account name 120 model -- the data model (default TreeFilterModel) 121 """ 122 if model is None: 123 model = self.modelfilter 124 if model is None: 125 return 126 127 if self.regroup: 128 name = 'MERGED' 129 if name not in self._iters: 130 return None 131 it = self._iters[name]['account'] 132 133 if model == self.model or it is None: 134 return it 135 try: 136 (ok, it) = self.modelfilter.convert_child_iter_to_iter(it) 137 if ok: 138 return it 139 return None 140 except RuntimeError: 141 return None 142 143 144 def _get_group_iter(self, name, account, model=None): 145 """ 146 Return the Gtk.TreeIter of the given group or None if not found 147 148 Keyword arguments: 149 name -- the group name 150 account -- the account name 151 model -- the data model (default TreeFilterModel) 152 """ 153 if model is None: 154 model = self.modelfilter 155 if model is None: 156 return 157 158 if self.regroup: 159 account = 'MERGED' 160 161 if account not in self._iters: 162 return None 163 if name not in self._iters[account]['groups']: 164 return None 165 166 it = self._iters[account]['groups'][name] 167 if model == self.model or it is None: 168 return it 169 try: 170 (ok, it) = self.modelfilter.convert_child_iter_to_iter(it) 171 if ok: 172 return it 173 return None 174 except RuntimeError: 175 return None 176 177 178 def _get_self_contact_iter(self, account, model=None): 179 """ 180 Return the Gtk.TreeIter of SelfContact or None if not found 181 182 Keyword arguments: 183 account -- the account of SelfContact 184 model -- the data model (default TreeFilterModel) 185 """ 186 jid = app.get_jid_from_account(account) 187 its = self._get_contact_iter(jid, account, model=model) 188 if its: 189 return its[0] 190 return None 191 192 193 def _get_contact_iter(self, jid, account, contact=None, model=None): 194 """ 195 Return a list of Gtk.TreeIter of the given contact 196 197 Keyword arguments: 198 jid -- the jid without resource 199 account -- the account 200 contact -- the contact (default None) 201 model -- the data model (default TreeFilterModel) 202 """ 203 if model is None: 204 model = self.modelfilter 205 # when closing Gajim model can be none (async pbs?) 206 if model is None: 207 return [] 208 209 if not contact: 210 contact = app.contacts.get_first_contact_from_jid(account, jid) 211 if not contact: 212 # We don't know this contact 213 return [] 214 215 if account not in self._iters: 216 return [] 217 218 if jid not in self._iters[account]['contacts']: 219 return [] 220 221 its = self._iters[account]['contacts'][jid] 222 223 if not its: 224 return [] 225 226 if model == self.model: 227 return its 228 229 its2 = [] 230 for it in its: 231 try: 232 (ok, it) = self.modelfilter.convert_child_iter_to_iter(it) 233 if ok: 234 its2.append(it) 235 except RuntimeError: 236 pass 237 return its2 238 239 @staticmethod 240 def _iter_is_separator(model, titer): 241 """ 242 Return True if the given iter is a separator 243 244 Keyword arguments: 245 model -- the data model 246 iter -- the Gtk.TreeIter to test 247 """ 248 if model[titer][0] == 'SEPARATOR': 249 return True 250 return False 251 252############################################################################# 253### Methods for adding and removing roster window items 254############################################################################# 255 256 def add_account(self, account): 257 """ 258 Add account to roster and draw it. Do nothing if it is already in 259 """ 260 if self._get_account_iter(account): 261 # Will happen on reconnect or for merged accounts 262 return 263 264 if self.regroup: 265 # Merged accounts view 266 show = helpers.get_global_show() 267 it = self.model.append(None, [get_icon_name(show), 268 _('Merged accounts'), 'account', '', 'all', None, None, None, 269 None, None, None, True] + [None] * self.nb_ext_renderers) 270 self._iters['MERGED']['account'] = it 271 else: 272 show = helpers.get_connection_status(account) 273 our_jid = app.get_jid_from_account(account) 274 275 it = self.model.append(None, [get_icon_name(show), 276 GLib.markup_escape_text(account), 'account', our_jid, 277 account, None, None, None, None, None, None, True] + 278 [None] * self.nb_ext_renderers) 279 self._iters[account]['account'] = it 280 281 self.draw_account(account) 282 283 284 def add_account_contacts(self, account, improve_speed=True, 285 draw_contacts=True): 286 """ 287 Add all contacts and groups of the given account to roster, draw them 288 and account 289 """ 290 if improve_speed: 291 self._before_fill() 292 jids = app.contacts.get_jid_list(account) 293 294 for jid in jids: 295 self.add_contact(jid, account) 296 297 if draw_contacts: 298 # Do not freeze the GUI when drawing the contacts 299 if jids: 300 # Overhead is big, only invoke when needed 301 self._idle_draw_jids_of_account(jids, account) 302 303 # Draw all known groups 304 for group in app.groups[account]: 305 self.draw_group(group, account) 306 self.draw_account(account) 307 308 if improve_speed: 309 self._after_fill() 310 311 def _add_group_iter(self, account, group): 312 """ 313 Add a group iter in roster and return the newly created iter 314 """ 315 if self.regroup: 316 account_group = 'MERGED' 317 else: 318 account_group = account 319 delimiter = app.connections[account].get_module('Delimiter').delimiter 320 group_splited = group.split(delimiter) 321 parent_group = delimiter.join(group_splited[:-1]) 322 if len(group_splited) > 1 and parent_group in self._iters[account_group]['groups']: 323 iter_parent = self._iters[account_group]['groups'][parent_group] 324 elif parent_group: 325 iter_parent = self._add_group_iter(account, parent_group) 326 if parent_group not in app.groups[account]: 327 if account + parent_group in self.collapsed_rows: 328 is_expanded = False 329 else: 330 is_expanded = True 331 app.groups[account][parent_group] = {'expand': is_expanded} 332 else: 333 iter_parent = self._get_account_iter(account, self.model) 334 iter_group = self.model.append(iter_parent, 335 [get_icon_name('closed'), 336 GLib.markup_escape_text(group), 'group', group, account, None, 337 None, None, None, None, None, False] + [None] * self.nb_ext_renderers) 338 self.draw_group(group, account) 339 self._iters[account_group]['groups'][group] = iter_group 340 return iter_group 341 342 def _add_entity(self, contact, account, groups=None, 343 big_brother_contact=None, big_brother_account=None): 344 """ 345 Add the given contact to roster data model 346 347 Contact is added regardless if he is already in roster or not. Return 348 list of newly added iters. 349 350 Keyword arguments: 351 contact -- the contact to add 352 account -- the contacts account 353 groups -- list of groups to add the contact to. 354 (default groups in contact.get_shown_groups()). 355 Parameter ignored when big_brother_contact is specified. 356 big_brother_contact -- if specified contact is added as child 357 big_brother_contact. (default None) 358 """ 359 added_iters = [] 360 visible = self.contact_is_visible(contact, account) 361 if big_brother_contact: 362 # Add contact under big brother 363 364 parent_iters = self._get_contact_iter( 365 big_brother_contact.jid, big_brother_account, 366 big_brother_contact, self.model) 367 368 # Do not confuse get_contact_iter: Sync groups of family members 369 contact.groups = big_brother_contact.groups[:] 370 371 image = self._get_avatar_image(account, contact.jid) 372 373 for child_iter in parent_iters: 374 it = self.model.append(child_iter, [None, 375 contact.get_shown_name(), 'contact', contact.jid, account, 376 None, None, None, None, image, None, visible] + \ 377 [None] * self.nb_ext_renderers) 378 added_iters.append(it) 379 if contact.jid in self._iters[account]['contacts']: 380 self._iters[account]['contacts'][contact.jid].append(it) 381 else: 382 self._iters[account]['contacts'][contact.jid] = [it] 383 else: 384 # We are a normal contact. Add us to our groups. 385 if not groups: 386 groups = contact.get_shown_groups() 387 for group in groups: 388 child_iterG = self._get_group_iter(group, account, 389 model=self.model) 390 if not child_iterG: 391 # Group is not yet in roster, add it! 392 child_iterG = self._add_group_iter(account, group) 393 394 if contact.is_transport(): 395 typestr = 'agent' 396 elif contact.is_groupchat: 397 typestr = 'groupchat' 398 else: 399 typestr = 'contact' 400 401 image = self._get_avatar_image(account, contact.jid) 402 403 # we add some values here. see draw_contact 404 # for more 405 i_ = self.model.append(child_iterG, [None, 406 contact.get_shown_name(), typestr, contact.jid, account, 407 None, None, None, None, image, None, visible] + \ 408 [None] * self.nb_ext_renderers) 409 added_iters.append(i_) 410 if contact.jid in self._iters[account]['contacts']: 411 self._iters[account]['contacts'][contact.jid].append(i_) 412 else: 413 self._iters[account]['contacts'][contact.jid] = [i_] 414 415 # Restore the group expand state 416 if account + group in self.collapsed_rows: 417 is_expanded = False 418 else: 419 is_expanded = True 420 if group not in app.groups[account]: 421 app.groups[account][group] = {'expand': is_expanded} 422 423 return added_iters 424 425 def _remove_entity(self, contact, account, groups=None): 426 """ 427 Remove the given contact from roster data model 428 429 Empty groups after contact removal are removed too. 430 Return False if contact still has children and deletion was 431 not performed. 432 Return True on success. 433 434 Keyword arguments: 435 contact -- the contact to add 436 account -- the contacts account 437 groups -- list of groups to remove the contact from. 438 """ 439 iters = self._get_contact_iter(contact.jid, account, contact, 440 self.model) 441 442 parent_iter = self.model.iter_parent(iters[0]) 443 parent_type = self.model[parent_iter][Column.TYPE] 444 445 if groups: 446 # Only remove from specified groups 447 all_iters = iters[:] 448 group_iters = [self._get_group_iter(group, account) 449 for group in groups] 450 iters = [titer for titer in all_iters 451 if self.model.iter_parent(titer) in group_iters] 452 453 iter_children = self.model.iter_children(iters[0]) 454 455 if iter_children: 456 # We have children. We cannot be removed! 457 return False 458 # Remove us and empty groups from the model 459 for i in iters: 460 parent_i = self.model.iter_parent(i) 461 parent_type = self.model[parent_i][Column.TYPE] 462 463 to_be_removed = i 464 while parent_type == 'group' and \ 465 self.model.iter_n_children(parent_i) == 1: 466 if self.regroup: 467 account_group = 'MERGED' 468 else: 469 account_group = account 470 group = self.model[parent_i][Column.JID] 471 if group in app.groups[account]: 472 del app.groups[account][group] 473 to_be_removed = parent_i 474 del self._iters[account_group]['groups'][group] 475 parent_i = self.model.iter_parent(parent_i) 476 parent_type = self.model[parent_i][Column.TYPE] 477 self.model.remove(to_be_removed) 478 479 del self._iters[account]['contacts'][contact.jid] 480 return True 481 482 def _add_metacontact_family(self, family, account): 483 """ 484 Add the give Metacontact family to roster data model 485 486 Add Big Brother to his groups and all others under him. 487 Return list of all added (contact, account) tuples with 488 Big Brother as first element. 489 490 Keyword arguments: 491 family -- the family, see Contacts.get_metacontacts_family() 492 """ 493 494 nearby_family, big_brother_jid, big_brother_account = \ 495 self._get_nearby_family_and_big_brother(family, account) 496 if not big_brother_jid: 497 return [] 498 big_brother_contact = app.contacts.get_first_contact_from_jid( 499 big_brother_account, big_brother_jid) 500 501 self._add_entity(big_brother_contact, big_brother_account) 502 503 brothers = [] 504 # Filter family members 505 for data in nearby_family: 506 _account = data['account'] 507 _jid = data['jid'] 508 _contact = app.contacts.get_first_contact_from_jid( 509 _account, _jid) 510 511 if not _contact or _contact == big_brother_contact: 512 # Corresponding account is not connected 513 # or brother already added 514 continue 515 516 self._add_entity(_contact, _account, 517 big_brother_contact=big_brother_contact, 518 big_brother_account=big_brother_account) 519 brothers.append((_contact, _account)) 520 521 brothers.insert(0, (big_brother_contact, big_brother_account)) 522 return brothers 523 524 def _remove_metacontact_family(self, family, account): 525 """ 526 Remove the given Metacontact family from roster data model 527 528 See Contacts.get_metacontacts_family() and 529 RosterWindow._remove_entity() 530 """ 531 nearby_family = self._get_nearby_family_and_big_brother( 532 family, account)[0] 533 534 # Family might has changed (actual big brother not on top). 535 # Remove children first then big brother 536 family_in_roster = False 537 for data in nearby_family: 538 _account = data['account'] 539 _jid = data['jid'] 540 _contact = app.contacts.get_first_contact_from_jid(_account, _jid) 541 542 iters = self._get_contact_iter(_jid, _account, _contact, self.model) 543 if not iters or not _contact: 544 # Family might not be up to date. 545 # Only try to remove what is actually in the roster 546 continue 547 548 family_in_roster = True 549 550 parent_iter = self.model.iter_parent(iters[0]) 551 parent_type = self.model[parent_iter][Column.TYPE] 552 553 if parent_type != 'contact': 554 # The contact on top 555 old_big_account = _account 556 old_big_contact = _contact 557 continue 558 559 self._remove_entity(_contact, _account) 560 561 if not family_in_roster: 562 return False 563 564 self._remove_entity(old_big_contact, old_big_account) 565 566 return True 567 568 def _recalibrate_metacontact_family(self, family, account): 569 """ 570 Regroup metacontact family if necessary 571 """ 572 573 brothers = [] 574 nearby_family, big_brother_jid, big_brother_account = \ 575 self._get_nearby_family_and_big_brother(family, account) 576 big_brother_contact = app.contacts.get_contact(big_brother_account, 577 big_brother_jid) 578 child_iters = self._get_contact_iter(big_brother_jid, 579 big_brother_account, model=self.model) 580 if child_iters: 581 parent_iter = self.model.iter_parent(child_iters[0]) 582 parent_type = self.model[parent_iter][Column.TYPE] 583 584 # Check if the current BigBrother has even been before. 585 if parent_type == 'contact': 586 for data in nearby_family: 587 # recalibrate after remove to keep highlight 588 if data['jid'] in app.to_be_removed[data['account']]: 589 return 590 591 self._remove_metacontact_family(family, account) 592 brothers = self._add_metacontact_family(family, account) 593 594 for c, acc in brothers: 595 self.draw_completely(c.jid, acc) 596 597 # Check is small brothers are under the big brother 598 for child in nearby_family: 599 _jid = child['jid'] 600 _account = child['account'] 601 if _account == big_brother_account and _jid == big_brother_jid: 602 continue 603 child_iters = self._get_contact_iter(_jid, _account, 604 model=self.model) 605 if not child_iters: 606 continue 607 parent_iter = self.model.iter_parent(child_iters[0]) 608 parent_type = self.model[parent_iter][Column.TYPE] 609 if parent_type != 'contact': 610 _contact = app.contacts.get_contact(_account, _jid) 611 self._remove_entity(_contact, _account) 612 self._add_entity(_contact, _account, groups=None, 613 big_brother_contact=big_brother_contact, 614 big_brother_account=big_brother_account) 615 616 def _get_nearby_family_and_big_brother(self, family, account): 617 return app.contacts.get_nearby_family_and_big_brother(family, account) 618 619 def _add_self_contact(self, account): 620 """ 621 Add account's SelfContact to roster and draw it and the account 622 623 Return the SelfContact contact instance 624 """ 625 jid = app.get_jid_from_account(account) 626 contact = app.contacts.get_first_contact_from_jid(account, jid) 627 628 child_iterA = self._get_account_iter(account, self.model) 629 self._iters[account]['contacts'][jid] = [self.model.append(child_iterA, 630 [None, app.nicks[account], 'self_contact', jid, account, None, 631 None, None, None, None, None, True] + [None] * self.nb_ext_renderers)] 632 633 self.draw_completely(jid, account) 634 self.draw_account(account) 635 636 return contact 637 638 def redraw_metacontacts(self, account): 639 for family in app.contacts.iter_metacontacts_families(account): 640 self._recalibrate_metacontact_family(family, account) 641 642 def add_contact(self, jid, account): 643 """ 644 Add contact to roster and draw him 645 646 Add contact to all its group and redraw the groups, the contact and the 647 account. If it's a Metacontact, add and draw the whole family. 648 Do nothing if the contact is already in roster. 649 650 Return the added contact instance. If it is a Metacontact return 651 Big Brother. 652 653 Keyword arguments: 654 jid -- the contact's jid or SelfJid to add SelfContact 655 account -- the corresponding account. 656 """ 657 contact = app.contacts.get_contact_with_highest_priority(account, jid) 658 if self._get_contact_iter(jid, account, contact, self.model): 659 # If contact already in roster, do nothing 660 return 661 662 if jid == app.get_jid_from_account(account): 663 return self._add_self_contact(account) 664 665 is_observer = contact.is_observer() 666 if is_observer: 667 # if he has a tag, remove it 668 app.contacts.remove_metacontact(account, jid) 669 670 # Add contact to roster 671 family = app.contacts.get_metacontacts_family(account, jid) 672 contacts = [] 673 if family: 674 # We have a family. So we are a metacontact. 675 # Add all family members that we shall be grouped with 676 if self.regroup: 677 # remove existing family members to regroup them 678 self._remove_metacontact_family(family, account) 679 contacts = self._add_metacontact_family(family, account) 680 else: 681 # We are a normal contact 682 contacts = [(contact, account), ] 683 self._add_entity(contact, account) 684 685 # Draw the contact and its groups contact 686 if not self.starting: 687 for c, acc in contacts: 688 self.draw_completely(c.jid, acc) 689 for group in contact.get_shown_groups(): 690 self.draw_group(group, account) 691 self._adjust_group_expand_collapse_state(group, account) 692 self.draw_account(account) 693 694 return contacts[0][0] # it's contact/big brother with highest priority 695 696 def remove_contact(self, jid, account, force=False, backend=False, maximize=False): 697 """ 698 Remove contact from roster 699 700 Remove contact from all its group. Remove empty groups or redraw 701 otherwise. 702 Draw the account. 703 If it's a Metacontact, remove the whole family. 704 Do nothing if the contact is not in roster. 705 706 Keyword arguments: 707 jid -- the contact's jid or SelfJid to remove SelfContact 708 account -- the corresponding account. 709 force -- remove contact even it has pending evens (Default False) 710 backend -- also remove contact instance (Default False) 711 """ 712 contact = app.contacts.get_contact_with_highest_priority(account, jid) 713 if not contact: 714 return 715 716 if not force and self.contact_has_pending_roster_events(contact, 717 account): 718 return False 719 720 iters = self._get_contact_iter(jid, account, contact, self.model) 721 if iters: 722 # no more pending events 723 # Remove contact from roster directly 724 family = app.contacts.get_metacontacts_family(account, jid) 725 if family: 726 # We have a family. So we are a metacontact. 727 self._remove_metacontact_family(family, account) 728 else: 729 self._remove_entity(contact, account) 730 731 old_grps = [] 732 if backend: 733 if not app.interface.msg_win_mgr.get_control(jid, account) or \ 734 force: 735 # If a window is still opened: don't remove contact instance 736 # Remove contact before redrawing, otherwise the old 737 # numbers will still be show 738 if not maximize: 739 # Don't remove contact when we maximize a room 740 app.contacts.remove_jid(account, jid, remove_meta=True) 741 if iters: 742 rest_of_family = [data for data in family 743 if account != data['account'] or jid != data['jid']] 744 if rest_of_family: 745 # reshow the rest of the family 746 brothers = self._add_metacontact_family(rest_of_family, 747 account) 748 for c, acc in brothers: 749 self.draw_completely(c.jid, acc) 750 else: 751 for c in app.contacts.get_contacts(account, jid): 752 c.sub = 'none' 753 c.show = 'not in roster' 754 c.status = '' 755 old_grps = c.get_shown_groups() 756 c.groups = [_('Not in contact list')] 757 self._add_entity(c, account) 758 self.draw_contact(jid, account) 759 760 if iters: 761 # Draw all groups of the contact 762 for group in contact.get_shown_groups() + old_grps: 763 self.draw_group(group, account) 764 self.draw_account(account) 765 766 return True 767 768 def rename_self_contact(self, old_jid, new_jid, account): 769 """ 770 Rename the self_contact jid 771 772 Keyword arguments: 773 old_jid -- our old jid 774 new_jid -- our new jid 775 account -- the corresponding account. 776 """ 777 app.contacts.change_contact_jid(old_jid, new_jid, account) 778 self_iter = self._get_self_contact_iter(account, model=self.model) 779 if not self_iter: 780 return 781 self.model[self_iter][Column.JID] = new_jid 782 self.draw_contact(new_jid, account) 783 784 def minimize_groupchat(self, account, jid, status=''): 785 gc_control = app.interface.msg_win_mgr.get_gc_control(jid, account) 786 app.interface.minimized_controls[account][jid] = gc_control 787 self.add_groupchat(jid, account) 788 789 def add_groupchat(self, jid, account): 790 """ 791 Add groupchat to roster and draw it. Return the added contact instance 792 """ 793 contact = app.contacts.get_groupchat_contact(account, jid) 794 show = 'offline' 795 if app.account_is_available(account): 796 show = 'online' 797 798 contact.show = show 799 self.add_contact(jid, account) 800 801 return contact 802 803 def remove_groupchat(self, jid, account, maximize=False): 804 """ 805 Remove groupchat from roster and redraw account and group 806 """ 807 contact = app.contacts.get_contact_with_highest_priority(account, jid) 808 if contact.is_groupchat: 809 if jid in app.interface.minimized_controls[account]: 810 del app.interface.minimized_controls[account][jid] 811 self.remove_contact(jid, account, force=True, backend=True, maximize=maximize) 812 return True 813 return False 814 815 # FIXME: This function is yet unused! Port to new API 816 def add_transport(self, jid, account): 817 """ 818 Add transport to roster and draw it. Return the added contact instance 819 """ 820 contact = app.contacts.get_contact_with_highest_priority(account, jid) 821 if contact is None: 822 contact = app.contacts.create_contact(jid=jid, account=account, 823 name=jid, groups=[_('Transports')], show='offline', 824 status='offline', sub='from') 825 app.contacts.add_contact(account, contact) 826 self.add_contact(jid, account) 827 return contact 828 829 def remove_transport(self, jid, account): 830 """ 831 Remove transport from roster and redraw account and group 832 """ 833 self.remove_contact(jid, account, force=True, backend=True) 834 return True 835 836 def rename_group(self, old_name, new_name, account): 837 """ 838 Rename a roster group 839 """ 840 if old_name == new_name: 841 return 842 843 # Groups may not change name from or to a special groups 844 for g in helpers.special_groups: 845 if g in (new_name, old_name): 846 return 847 848 # update all contacts in the given group 849 if self.regroup: 850 accounts = app.connections.keys() 851 else: 852 accounts = [account, ] 853 854 for acc in accounts: 855 changed_contacts = [] 856 for jid in app.contacts.get_jid_list(acc): 857 contact = app.contacts.get_first_contact_from_jid(acc, jid) 858 if old_name not in contact.groups: 859 continue 860 861 self.remove_contact(jid, acc, force=True) 862 863 contact.groups.remove(old_name) 864 if new_name not in contact.groups: 865 contact.groups.append(new_name) 866 867 changed_contacts.append({'jid': jid, 'name': contact.name, 868 'groups':contact.groups}) 869 870 app.connections[acc].get_module('Roster').update_contacts( 871 changed_contacts) 872 873 for c in changed_contacts: 874 self.add_contact(c['jid'], acc) 875 876 self._adjust_group_expand_collapse_state(new_name, acc) 877 878 self.draw_group(old_name, acc) 879 self.draw_group(new_name, acc) 880 881 882 def add_contact_to_groups(self, jid, account, groups, update=True): 883 """ 884 Add contact to given groups and redraw them 885 886 Contact on server is updated too. When the contact has a family, 887 the action will be performed for all members. 888 889 Keyword Arguments: 890 jid -- the jid 891 account -- the corresponding account 892 groups -- list of Groups to add the contact to. 893 update -- update contact on the server 894 """ 895 self.remove_contact(jid, account, force=True) 896 for contact in app.contacts.get_contacts(account, jid): 897 for group in groups: 898 if group not in contact.groups: 899 # we might be dropped from meta to group 900 contact.groups.append(group) 901 if update: 902 con = app.connections[account] 903 con.get_module('Roster').update_contact( 904 jid, contact.name, contact.groups) 905 906 self.add_contact(jid, account) 907 908 for group in groups: 909 self._adjust_group_expand_collapse_state(group, account) 910 911 def remove_contact_from_groups(self, jid, account, groups, update=True): 912 """ 913 Remove contact from given groups and redraw them 914 915 Contact on server is updated too. When the contact has a family, 916 the action will be performed for all members. 917 918 Keyword Arguments: 919 jid -- the jid 920 account -- the corresponding account 921 groups -- list of Groups to remove the contact from 922 update -- update contact on the server 923 """ 924 self.remove_contact(jid, account, force=True) 925 for contact in app.contacts.get_contacts(account, jid): 926 for group in groups: 927 if group in contact.groups: 928 # Needed when we remove from "General" or "Observers" 929 contact.groups.remove(group) 930 if update: 931 con = app.connections[account] 932 con.get_module('Roster').update_contact( 933 jid, contact.name, contact.groups) 934 self.add_contact(jid, account) 935 936 # Also redraw old groups 937 for group in groups: 938 self.draw_group(group, account) 939 940 # FIXME: maybe move to app.py 941 def remove_newly_added(self, jid, account): 942 if account not in app.newly_added: 943 # Account has been deleted during the timeout that called us 944 return 945 if jid in app.newly_added[account]: 946 app.newly_added[account].remove(jid) 947 self.draw_contact(jid, account) 948 949 # FIXME: maybe move to app.py 950 def remove_to_be_removed(self, jid, account): 951 if account not in app.interface.instances: 952 # Account has been deleted during the timeout that called us 953 return 954 if jid in app.newly_added[account]: 955 return 956 if jid in app.to_be_removed[account]: 957 app.to_be_removed[account].remove(jid) 958 family = app.contacts.get_metacontacts_family(account, jid) 959 if family: 960 # Perform delayed recalibration 961 self._recalibrate_metacontact_family(family, account) 962 self.draw_contact(jid, account) 963 # Hide Group if all children are hidden 964 contact = app.contacts.get_contact(account, jid) 965 if not contact: 966 return 967 for group in contact.get_shown_groups(): 968 self.draw_group(group, account) 969 970 # FIXME: integrate into add_contact() 971 def add_to_not_in_the_roster(self, account, jid, nick='', resource='', 972 groupchat=False): 973 contact = app.contacts.create_not_in_roster_contact( 974 jid=jid, account=account, resource=resource, name=nick, 975 groupchat=groupchat) 976 app.contacts.add_contact(account, contact) 977 self.add_contact(contact.jid, account) 978 return contact 979 980 981################################################################################ 982### Methods for adding and removing roster window items 983################################################################################ 984 985 def _really_draw_account(self, account): 986 child_iter = self._get_account_iter(account, self.model) 987 if not child_iter: 988 return 989 990 if self.regroup: 991 account_name = _('Merged accounts') 992 accounts = [] 993 else: 994 account_name = app.get_account_label(account) 995 accounts = [account] 996 997 if account in self.collapsed_rows and \ 998 self.model.iter_has_child(child_iter): 999 account_name = '[%s]' % account_name 1000 1001 if (app.account_is_available(account) or (self.regroup and \ 1002 app.get_number_of_connected_accounts())) and app.settings.get( 1003 'show_contacts_number'): 1004 nbr_on, nbr_total = app.contacts.get_nb_online_total_contacts( 1005 accounts=accounts) 1006 account_name += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total)) 1007 1008 self.model[child_iter][Column.NAME] = GLib.markup_escape_text(account_name) 1009 1010 mood_icon_name = get_account_mood_icon_name(account) 1011 self.model[child_iter][Column.MOOD_PIXBUF] = mood_icon_name 1012 1013 activity_icon_name = get_account_activity_icon_name(account) 1014 self.model[child_iter][Column.ACTIVITY_PIXBUF] = activity_icon_name 1015 1016 tune_icon_name = get_account_tune_icon_name(account) 1017 self.model[child_iter][Column.TUNE_ICON] = tune_icon_name 1018 1019 location_icon_name = get_account_location_icon_name(account) 1020 self.model[child_iter][Column.LOCATION_ICON] = location_icon_name 1021 1022 def _really_draw_accounts(self): 1023 for acct in self.accounts_to_draw: 1024 self._really_draw_account(acct) 1025 self.accounts_to_draw = [] 1026 return False 1027 1028 def draw_account(self, account): 1029 if account in self.accounts_to_draw: 1030 return 1031 self.accounts_to_draw.append(account) 1032 if len(self.accounts_to_draw) == 1: 1033 GLib.timeout_add(200, self._really_draw_accounts) 1034 1035 def _really_draw_group(self, group, account): 1036 child_iter = self._get_group_iter(group, account, model=self.model) 1037 if not child_iter: 1038 # Eg. We redraw groups after we removed a entity 1039 # and its empty groups 1040 return 1041 if self.regroup: 1042 accounts = [] 1043 else: 1044 accounts = [account] 1045 text = GLib.markup_escape_text(group) 1046 if app.settings.get('show_contacts_number'): 1047 nbr_on, nbr_total = app.contacts.get_nb_online_total_contacts( 1048 accounts=accounts, groups=[group]) 1049 text += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total)) 1050 1051 self.model[child_iter][Column.NAME] = text 1052 1053 # Hide group if no more contacts 1054 iterG = self._get_group_iter(group, account, model=self.modelfilter) 1055 to_hide = [] 1056 while iterG: 1057 parent = self.modelfilter.iter_parent(iterG) 1058 if (not self.modelfilter.iter_has_child(iterG)) or (to_hide \ 1059 and self.modelfilter.iter_n_children(iterG) == 1): 1060 to_hide.append(iterG) 1061 if not parent or self.modelfilter[parent][Column.TYPE] != \ 1062 'group': 1063 iterG = None 1064 else: 1065 iterG = parent 1066 else: 1067 iterG = None 1068 for iter_ in to_hide: 1069 self.modelfilter[iter_][Column.VISIBLE] = False 1070 1071 def _really_draw_groups(self): 1072 for ag in self.groups_to_draw.values(): 1073 acct = ag['account'] 1074 grp = ag['group'] 1075 self._really_draw_group(grp, acct) 1076 self.groups_to_draw = {} 1077 return False 1078 1079 def draw_group(self, group, account): 1080 ag = account + group 1081 if ag in self.groups_to_draw: 1082 return 1083 self.groups_to_draw[ag] = {'group': group, 'account': account} 1084 if len(self.groups_to_draw) == 1: 1085 GLib.timeout_add(200, self._really_draw_groups) 1086 1087 def draw_parent_contact(self, jid, account): 1088 child_iters = self._get_contact_iter(jid, account, model=self.model) 1089 if not child_iters: 1090 return False 1091 parent_iter = self.model.iter_parent(child_iters[0]) 1092 if self.model[parent_iter][Column.TYPE] != 'contact': 1093 # parent is not a contact 1094 return 1095 parent_jid = self.model[parent_iter][Column.JID] 1096 parent_account = self.model[parent_iter][Column.ACCOUNT] 1097 self.draw_contact(parent_jid, parent_account) 1098 return False 1099 1100 def draw_contact(self, jid, account, selected=False, focus=False, 1101 contact_instances=None, contact=None): 1102 """ 1103 Draw the correct state image, name BUT not avatar 1104 """ 1105 # focus is about if the roster window has toplevel-focus or not 1106 # FIXME: We really need a custom cell_renderer 1107 1108 if not contact_instances: 1109 contact_instances = app.contacts.get_contacts(account, jid) 1110 if not contact: 1111 contact = app.contacts.get_highest_prio_contact_from_contacts( 1112 contact_instances) 1113 if not contact: 1114 return False 1115 1116 child_iters = self._get_contact_iter(jid, account, contact, self.model) 1117 if not child_iters: 1118 return False 1119 1120 name = GLib.markup_escape_text(contact.get_shown_name()) 1121 1122 # gets number of unread gc marked messages 1123 if jid in app.interface.minimized_controls[account] and \ 1124 app.interface.minimized_controls[account][jid]: 1125 nb_unread = len(app.events.get_events(account, jid, 1126 ['printed_marked_gc_msg'])) 1127 nb_unread += app.interface.minimized_controls \ 1128 [account][jid].get_nb_unread_pm() 1129 1130 if nb_unread == 1: 1131 name = '%s *' % name 1132 elif nb_unread > 1: 1133 name = '%s [%s]' % (name, str(nb_unread)) 1134 1135 # Strike name if blocked 1136 strike = helpers.jid_is_blocked(account, jid) 1137 if strike: 1138 name = '<span strikethrough="true">%s</span>' % name 1139 1140 # Show resource counter 1141 nb_connected_contact = 0 1142 for c in contact_instances: 1143 if c.show not in ('error', 'offline'): 1144 nb_connected_contact += 1 1145 if nb_connected_contact > 1: 1146 # switch back to default writing direction 1147 name += i18n.paragraph_direction_mark(name) 1148 name += ' (%d)' % nb_connected_contact 1149 1150 # add status msg, if not empty, under contact name in 1151 # the treeview 1152 if app.settings.get('show_status_msgs_in_roster'): 1153 status_span = '\n<span size="small" style="italic" ' \ 1154 'alpha="70%">{}</span>' 1155 if contact.is_groupchat: 1156 disco_info = app.storage.cache.get_last_disco_info(contact.jid) 1157 if disco_info is not None: 1158 description = disco_info.muc_description 1159 if description: 1160 name += status_span.format( 1161 GLib.markup_escape_text(description)) 1162 elif contact.status: 1163 status = contact.status.strip() 1164 if status != '': 1165 status = helpers.reduce_chars_newlines( 1166 status, max_lines=1) 1167 name += status_span.format( 1168 GLib.markup_escape_text(status)) 1169 1170 icon_name = helpers.get_icon_name_to_show(contact, account) 1171 # look if another resource has awaiting events 1172 for c in contact_instances: 1173 c_icon_name = helpers.get_icon_name_to_show(c, account) 1174 if c_icon_name in ('event', 'muc-active', 'muc-inactive'): 1175 icon_name = c_icon_name 1176 break 1177 1178 # Check for events of collapsed (hidden) brothers 1179 family = app.contacts.get_metacontacts_family(account, jid) 1180 is_big_brother = False 1181 have_visible_children = False 1182 if family: 1183 bb_jid, bb_account = \ 1184 self._get_nearby_family_and_big_brother(family, account)[1:] 1185 is_big_brother = (jid, account) == (bb_jid, bb_account) 1186 iters = self._get_contact_iter(jid, account) 1187 have_visible_children = iters and \ 1188 self.modelfilter.iter_has_child(iters[0]) 1189 1190 if have_visible_children: 1191 # We are the big brother and have a visible family 1192 for child_iter in child_iters: 1193 child_path = self.model.get_path(child_iter) 1194 path = self.modelfilter.convert_child_path_to_path(child_path) 1195 1196 if not path: 1197 continue 1198 1199 if not self.tree.row_expanded(path) and icon_name != 'event': 1200 iterC = self.model.iter_children(child_iter) 1201 while iterC: 1202 # a child has awaiting messages? 1203 jidC = self.model[iterC][Column.JID] 1204 accountC = self.model[iterC][Column.ACCOUNT] 1205 if app.events.get_events(accountC, jidC): 1206 icon_name = 'event' 1207 break 1208 iterC = self.model.iter_next(iterC) 1209 1210 if self.tree.row_expanded(path): 1211 icon_name += ':opened' 1212 else: 1213 icon_name += ':closed' 1214 1215 theme_icon = get_icon_name(icon_name) 1216 self.model[child_iter][Column.IMG] = theme_icon 1217 self.model[child_iter][Column.NAME] = name 1218 #TODO: compute visible 1219 visible = True 1220 self.model[child_iter][Column.VISIBLE] = visible 1221 else: 1222 # A normal contact or little brother 1223 transport = app.get_transport_name_from_jid(jid) 1224 if transport == 'jabber': 1225 transport = None 1226 theme_icon = get_icon_name(icon_name, transport=transport) 1227 1228 visible = self.contact_is_visible(contact, account) 1229 # All iters have the same icon (no expand/collapse) 1230 for child_iter in child_iters: 1231 self.model[child_iter][Column.IMG] = theme_icon 1232 self.model[child_iter][Column.NAME] = name 1233 self.model[child_iter][Column.VISIBLE] = visible 1234 if visible: 1235 parent_iter = self.model.iter_parent(child_iter) 1236 self.model[parent_iter][Column.VISIBLE] = True 1237 1238 # We are a little brother 1239 if family and not is_big_brother and not self.starting: 1240 self.draw_parent_contact(jid, account) 1241 1242 if visible: 1243 delimiter = app.connections[account].get_module('Delimiter').delimiter 1244 for group in contact.get_shown_groups(): 1245 group_splited = group.split(delimiter) 1246 i = 1 1247 while i < len(group_splited) + 1: 1248 g = delimiter.join(group_splited[:i]) 1249 iterG = self._get_group_iter(g, account, model=self.model) 1250 if iterG: 1251 # it's not self contact 1252 self.model[iterG][Column.VISIBLE] = True 1253 i += 1 1254 1255 app.plugin_manager.gui_extension_point('roster_draw_contact', self, 1256 jid, account, contact) 1257 1258 return False 1259 1260 def _is_pep_shown_in_roster(self, pep_type): 1261 if pep_type == PEPEventType.MOOD: 1262 return app.settings.get('show_mood_in_roster') 1263 1264 if pep_type == PEPEventType.ACTIVITY: 1265 return app.settings.get('show_activity_in_roster') 1266 1267 if pep_type == PEPEventType.TUNE: 1268 return app.settings.get('show_tunes_in_roster') 1269 1270 if pep_type == PEPEventType.LOCATION: 1271 return app.settings.get('show_location_in_roster') 1272 1273 return False 1274 1275 def draw_all_pep_types(self, jid, account, contact=None): 1276 self._draw_pep(account, jid, PEPEventType.MOOD) 1277 self._draw_pep(account, jid, PEPEventType.ACTIVITY) 1278 self._draw_pep(account, jid, PEPEventType.TUNE) 1279 self._draw_pep(account, jid, PEPEventType.LOCATION) 1280 1281 def _draw_pep(self, account, jid, type_): 1282 if not self._is_pep_shown_in_roster(type_): 1283 return 1284 1285 iters = self._get_contact_iter(jid, account, model=self.model) 1286 if not iters: 1287 return 1288 contact = app.contacts.get_contact(account, jid) 1289 1290 icon = None 1291 data = contact.pep.get(type_) 1292 1293 if type_ == PEPEventType.MOOD: 1294 column = Column.MOOD_PIXBUF 1295 if data is not None: 1296 icon = 'mood-%s' % data.mood 1297 elif type_ == PEPEventType.ACTIVITY: 1298 column = Column.ACTIVITY_PIXBUF 1299 if data is not None: 1300 icon = get_activity_icon_name(data.activity, data.subactivity) 1301 elif type_ == PEPEventType.TUNE: 1302 column = Column.TUNE_ICON 1303 if data is not None: 1304 icon = 'audio-x-generic' 1305 elif type_ == PEPEventType.LOCATION: 1306 column = Column.LOCATION_ICON 1307 if data is not None: 1308 icon = 'applications-internet' 1309 1310 for child_iter in iters: 1311 self.model[child_iter][column] = icon 1312 1313 def _get_avatar_image(self, account, jid): 1314 if not app.settings.get('show_avatars_in_roster'): 1315 return None 1316 scale = self.window.get_scale_factor() 1317 surface = app.contacts.get_avatar( 1318 account, jid, AvatarSize.ROSTER, scale) 1319 return Gtk.Image.new_from_surface(surface) 1320 1321 def draw_avatar(self, jid, account): 1322 iters = self._get_contact_iter(jid, account, model=self.model) 1323 if not iters or not app.settings.get('show_avatars_in_roster'): 1324 return 1325 jid = self.model[iters[0]][Column.JID] 1326 image = self._get_avatar_image(account, jid) 1327 1328 for child_iter in iters: 1329 self.model[child_iter][Column.AVATAR_IMG] = image 1330 return False 1331 1332 def draw_completely(self, jid, account): 1333 contact_instances = app.contacts.get_contacts(account, jid) 1334 contact = app.contacts.get_highest_prio_contact_from_contacts( 1335 contact_instances) 1336 self.draw_contact( 1337 jid, account, 1338 contact_instances=contact_instances, 1339 contact=contact) 1340 1341 def adjust_and_draw_contact_context(self, jid, account): 1342 """ 1343 Draw contact, account and groups of given jid Show contact if it has 1344 pending events 1345 """ 1346 contact = app.contacts.get_first_contact_from_jid(account, jid) 1347 if not contact: 1348 # idle draw or just removed SelfContact 1349 return 1350 1351 family = app.contacts.get_metacontacts_family(account, jid) 1352 if family: 1353 # There might be a new big brother 1354 self._recalibrate_metacontact_family(family, account) 1355 self.draw_contact(jid, account) 1356 self.draw_account(account) 1357 1358 for group in contact.get_shown_groups(): 1359 self.draw_group(group, account) 1360 self._adjust_group_expand_collapse_state(group, account) 1361 1362 def _idle_draw_jids_of_account(self, jids, account): 1363 """ 1364 Draw given contacts and their avatars in a lazy fashion 1365 1366 Keyword arguments: 1367 jids -- a list of jids to draw 1368 account -- the corresponding account 1369 """ 1370 def _draw_all_contacts(jids, account): 1371 for jid in jids: 1372 family = app.contacts.get_metacontacts_family(account, jid) 1373 if family: 1374 # For metacontacts over several accounts: 1375 # When we connect a new account existing brothers 1376 # must be redrawn (got removed and added again) 1377 for data in family: 1378 self.draw_completely(data['jid'], data['account']) 1379 else: 1380 self.draw_completely(jid, account) 1381 yield True 1382 self.refilter_shown_roster_items() 1383 yield False 1384 1385 task = _draw_all_contacts(jids, account) 1386 GLib.idle_add(next, task) 1387 1388 def _before_fill(self): 1389 self.tree.freeze_child_notify() 1390 self.tree.set_model(None) 1391 # disable sorting 1392 self.model.set_sort_column_id(-2, Gtk.SortType.ASCENDING) 1393 self.starting = True 1394 self.starting_filtering = True 1395 1396 def _after_fill(self): 1397 self.starting = False 1398 accounts_list = app.contacts.get_accounts() 1399 for account in app.connections: 1400 if account not in accounts_list: 1401 continue 1402 1403 jids = app.contacts.get_jid_list(account) 1404 for jid in jids: 1405 self.draw_completely(jid, account) 1406 1407 # Draw all known groups 1408 for group in app.groups[account]: 1409 self.draw_group(group, account) 1410 self.draw_account(account) 1411 1412 self.model.set_sort_column_id(1, Gtk.SortType.ASCENDING) 1413 self.tree.set_model(self.modelfilter) 1414 self.tree.thaw_child_notify() 1415 self.starting_filtering = False 1416 self.refilter_shown_roster_items() 1417 1418 def setup_and_draw_roster(self): 1419 """ 1420 Create new empty model and draw roster 1421 """ 1422 self.modelfilter = None 1423 self.model = Gtk.TreeStore(*self.columns) 1424 1425 self.model.set_sort_func(1, self._compareIters) 1426 self.model.set_sort_column_id(1, Gtk.SortType.ASCENDING) 1427 self.modelfilter = self.model.filter_new() 1428 self.modelfilter.set_visible_func(self._visible_func) 1429 self.modelfilter.connect('row-has-child-toggled', 1430 self.on_modelfilter_row_has_child_toggled) 1431 self.tree.set_model(self.modelfilter) 1432 1433 self._iters = {} 1434 # for merged mode 1435 self._iters['MERGED'] = {'account': None, 'groups': {}} 1436 for acct in app.contacts.get_accounts(): 1437 self._iters[acct] = {'account': None, 'groups': {}, 'contacts': {}} 1438 1439 for acct in app.contacts.get_accounts(): 1440 self.add_account(acct) 1441 self.add_account_contacts(acct, improve_speed=True, 1442 draw_contacts=False) 1443 1444 # Recalculate column width for ellipsizing 1445 self.tree.columns_autosize() 1446 1447 def update_status_selector(self): 1448 self._status_selector.update() 1449 1450 def select_contact(self, jid, account): 1451 """ 1452 Select contact in roster. If contact is hidden but has events, show him 1453 """ 1454 # Refiltering SHOULD NOT be needed: 1455 # When a contact gets a new event he will be redrawn and his 1456 # icon changes, so _visible_func WILL be called on him anyway 1457 iters = self._get_contact_iter(jid, account) 1458 if not iters: 1459 # Not visible in roster 1460 return 1461 path = self.modelfilter.get_path(iters[0]) 1462 if self.dragging or not app.settings.get( 1463 'scroll_roster_to_last_message'): 1464 # do not change selection while DND'ing 1465 return 1466 # Expand his parent, so this path is visible, don't expand it. 1467 path.up() 1468 self.tree.expand_to_path(path) 1469 self.tree.scroll_to_cell(path) 1470 self.tree.set_cursor(path) 1471 1472 def _readjust_expand_collapse_state(self): 1473 def func(model, path, iter_, param): 1474 type_ = model[iter_][Column.TYPE] 1475 acct = model[iter_][Column.ACCOUNT] 1476 jid = model[iter_][Column.JID] 1477 key = None 1478 if type_ == 'account': 1479 key = acct 1480 elif type_ == 'group': 1481 key = acct + jid 1482 elif type_ == 'contact': 1483 parent_iter = model.iter_parent(iter_) 1484 ptype = model[parent_iter][Column.TYPE] 1485 if ptype == 'group': 1486 grp = model[parent_iter][Column.JID] 1487 key = acct + grp + jid 1488 if key: 1489 if key in self.collapsed_rows: 1490 self.tree.collapse_row(path) 1491 else: 1492 self.tree.expand_row(path, False) 1493 self.modelfilter.foreach(func, None) 1494 1495 def _adjust_account_expand_collapse_state(self, account): 1496 """ 1497 Expand/collapse account row based on self.collapsed_rows 1498 """ 1499 if not self.tree.get_model(): 1500 return 1501 iterA = self._get_account_iter(account) 1502 if not iterA: 1503 # thank you modelfilter 1504 return 1505 path = self.modelfilter.get_path(iterA) 1506 if account in self.collapsed_rows: 1507 self.tree.collapse_row(path) 1508 else: 1509 self.tree.expand_row(path, False) 1510 return False 1511 1512 1513 def _adjust_group_expand_collapse_state(self, group, account): 1514 """ 1515 Expand/collapse group row based on self.collapsed_rows 1516 """ 1517 if not self.tree.get_model(): 1518 return 1519 if account not in app.connections: 1520 return 1521 delimiter = app.connections[account].get_module('Delimiter').delimiter 1522 group_splited = group.split(delimiter) 1523 i = 1 1524 while i < len(group_splited) + 1: 1525 g = delimiter.join(group_splited[:i]) 1526 iterG = self._get_group_iter(g, account) 1527 if not iterG: 1528 # Group not visible 1529 return 1530 path = self.modelfilter.get_path(iterG) 1531 if account + g in self.collapsed_rows: 1532 self.tree.collapse_row(path) 1533 else: 1534 self.tree.expand_row(path, False) 1535 i += 1 1536 1537############################################################################## 1538### Roster and Modelfilter handling 1539############################################################################## 1540 1541 def refilter_shown_roster_items(self): 1542 if self.filtering: 1543 return 1544 self.filtering = True 1545 for account in app.connections: 1546 for jid in app.contacts.get_jid_list(account): 1547 self.adjust_and_draw_contact_context(jid, account) 1548 self.filtering = False 1549 1550 def contact_has_pending_roster_events(self, contact, account): 1551 """ 1552 Return True if the contact or one if it resources has pending events 1553 """ 1554 # jid has pending events 1555 if app.events.get_nb_roster_events(account, contact.jid) > 0: 1556 return True 1557 # check events of all resources 1558 for contact_ in app.contacts.get_contacts(account, contact.jid): 1559 if contact_.resource and app.events.get_nb_roster_events(account, 1560 contact_.get_full_jid()) > 0: 1561 return True 1562 return False 1563 1564 def contact_is_visible(self, contact, account): 1565 if self.rfilter_enabled: 1566 return self.rfilter_string in contact.get_shown_name().lower() 1567 if self.contact_has_pending_roster_events(contact, account): 1568 return True 1569 if app.settings.get('showoffline'): 1570 return True 1571 1572 if contact.show in ('offline', 'error'): 1573 if contact.jid in app.to_be_removed[account]: 1574 return True 1575 return False 1576 if app.settings.get('show_only_chat_and_online') and contact.show in ( 1577 'away', 'xa', 'busy'): 1578 return False 1579 if _('Transports') in contact.get_shown_groups(): 1580 return app.settings.get('show_transports_group') 1581 return True 1582 1583 def _visible_func(self, model, titer, dummy): 1584 """ 1585 Determine whether iter should be visible in the treeview 1586 """ 1587 if self.starting_filtering: 1588 return False 1589 1590 visible = model[titer][Column.VISIBLE] 1591 1592 type_ = model[titer][Column.TYPE] 1593 if not type_: 1594 return False 1595 if type_ == 'account': 1596 # Always show account 1597 return True 1598 1599 account = model[titer][Column.ACCOUNT] 1600 if not account: 1601 return False 1602 1603 jid = model[titer][Column.JID] 1604 if not jid: 1605 return False 1606 1607 if not self.rfilter_enabled: 1608 return visible 1609 1610 if type_ == 'group': 1611 group = jid 1612 if group == _('Transports'): 1613 if self.regroup: 1614 accounts = app.contacts.get_accounts() 1615 else: 1616 accounts = [account] 1617 for _acc in accounts: 1618 for contact in app.contacts.iter_contacts(_acc): 1619 if group in contact.get_shown_groups(): 1620 if self.rfilter_string in \ 1621 contact.get_shown_name().lower(): 1622 return True 1623 elif self.contact_has_pending_roster_events(contact, 1624 _acc): 1625 return True 1626 # No transport has been found 1627 return False 1628 1629 if type_ == 'contact': 1630 if model.iter_has_child(titer): 1631 iter_c = model.iter_children(titer) 1632 while iter_c: 1633 if self.rfilter_string in model[iter_c][Column.NAME].lower(): 1634 return True 1635 iter_c = model.iter_next(iter_c) 1636 return self.rfilter_string in model[titer][Column.NAME].lower() 1637 1638 if type_ == 'agent': 1639 return self.rfilter_string in model[titer][Column.NAME].lower() 1640 1641 if type_ == 'groupchat': 1642 return self.rfilter_string in model[titer][Column.NAME].lower() 1643 1644 return visible 1645 1646 def _compareIters(self, model, iter1, iter2, data=None): 1647 """ 1648 Compare two iters to sort them 1649 """ 1650 name1 = model[iter1][Column.NAME] 1651 name2 = model[iter2][Column.NAME] 1652 if not name1 or not name2: 1653 return 0 1654 type1 = model[iter1][Column.TYPE] 1655 type2 = model[iter2][Column.TYPE] 1656 if type1 == 'self_contact': 1657 return -1 1658 if type2 == 'self_contact': 1659 return 1 1660 if type1 == 'group': 1661 name1 = model[iter1][Column.JID] 1662 name2 = model[iter2][Column.JID] 1663 if name1 == _('Transports'): 1664 return 1 1665 if name2 == _('Transports'): 1666 return -1 1667 if name1 == _('Not in contact list'): 1668 return 1 1669 if name2 == _('Not in contact list'): 1670 return -1 1671 if name1 == _('Group chats'): 1672 return 1 1673 if name2 == _('Group chats'): 1674 return -1 1675 account1 = model[iter1][Column.ACCOUNT] 1676 account2 = model[iter2][Column.ACCOUNT] 1677 if not account1 or not account2: 1678 return 0 1679 if type1 == 'account': 1680 return locale.strcoll(account1, account2) 1681 jid1 = model[iter1][Column.JID] 1682 jid2 = model[iter2][Column.JID] 1683 if type1 == 'contact': 1684 lcontact1 = app.contacts.get_contacts(account1, jid1) 1685 contact1 = app.contacts.get_first_contact_from_jid(account1, jid1) 1686 if not contact1: 1687 return 0 1688 name1 = contact1.get_shown_name() 1689 if type2 == 'contact': 1690 lcontact2 = app.contacts.get_contacts(account2, jid2) 1691 contact2 = app.contacts.get_first_contact_from_jid(account2, jid2) 1692 if not contact2: 1693 return 0 1694 name2 = contact2.get_shown_name() 1695 # We first compare by show if sort_by_show_in_roster is True or if it's 1696 # a child contact 1697 if type1 == 'contact' and type2 == 'contact' and \ 1698 app.settings.get('sort_by_show_in_roster'): 1699 cshow = {'chat':0, 'online': 1, 'away': 2, 'xa': 3, 'dnd': 4, 1700 'offline': 6, 'not in roster': 7, 'error': 8} 1701 s = self.get_show(lcontact1) 1702 show1 = cshow.get(s, 9) 1703 s = self.get_show(lcontact2) 1704 show2 = cshow.get(s, 9) 1705 removing1 = False 1706 removing2 = False 1707 if show1 == 6 and jid1 in app.to_be_removed[account1]: 1708 removing1 = True 1709 if show2 == 6 and jid2 in app.to_be_removed[account2]: 1710 removing2 = True 1711 if removing1 and not removing2: 1712 return 1 1713 if removing2 and not removing1: 1714 return -1 1715 sub1 = contact1.sub 1716 sub2 = contact2.sub 1717 # none and from goes after 1718 if sub1 not in ['none', 'from'] and sub2 in ['none', 'from']: 1719 return -1 1720 if sub1 in ['none', 'from'] and sub2 not in ['none', 'from']: 1721 return 1 1722 if show1 < show2: 1723 return -1 1724 if show1 > show2: 1725 return 1 1726 # We compare names 1727 cmp_result = locale.strcoll(name1.lower(), name2.lower()) 1728 if cmp_result < 0: 1729 return -1 1730 if cmp_result > 0: 1731 return 1 1732 if type1 == 'contact' and type2 == 'contact': 1733 # We compare account names 1734 cmp_result = locale.strcoll(account1.lower(), account2.lower()) 1735 if cmp_result < 0: 1736 return -1 1737 if cmp_result > 0: 1738 return 1 1739 # We compare jids 1740 cmp_result = locale.strcoll(jid1.lower(), jid2.lower()) 1741 if cmp_result < 0: 1742 return -1 1743 if cmp_result > 0: 1744 return 1 1745 return 0 1746 1747################################################################################ 1748### FIXME: Methods that don't belong to roster window... 1749### ... at least not in there current form 1750################################################################################ 1751 1752 def fire_up_unread_messages_events(self, account): 1753 """ 1754 Read from db the unread messages, and fire them up, and if we find very 1755 old unread messages, delete them from unread table 1756 """ 1757 results = app.storage.archive.get_unread_msgs() 1758 for result, shown in results: 1759 jid = result.jid 1760 additional_data = result.additional_data 1761 if app.contacts.get_first_contact_from_jid(account, jid) and not \ 1762 shown: 1763 # We have this jid in our contacts list 1764 # XXX unread messages should probably have their session saved 1765 # with them 1766 session = app.connections[account].make_new_session(jid) 1767 1768 tim = float(result.time) 1769 session.roster_message(jid, result.message, tim, msg_type='chat', 1770 msg_log_id=result.log_line_id, additional_data=additional_data) 1771 app.storage.archive.set_shown_unread_msgs(result.log_line_id) 1772 1773 elif (time.time() - result.time) > 2592000: 1774 # ok, here we see that we have a message in unread messages 1775 # table that is older than a month. It is probably from someone 1776 # not in our roster for accounts we usually launch, so we will 1777 # delete this id from unread message tables. 1778 app.storage.archive.set_read_messages([result.log_line_id]) 1779 1780 def fill_contacts_and_groups_dicts(self, array, account): 1781 """ 1782 Fill app.contacts and app.groups 1783 """ 1784 # FIXME: This function needs to be split 1785 # Most of the logic SHOULD NOT be done at GUI level 1786 if account not in app.contacts.get_accounts(): 1787 app.contacts.add_account(account) 1788 if not account in self._iters: 1789 self._iters[account] = {'account': None, 'groups': {}, 1790 'contacts': {}} 1791 if account not in app.groups: 1792 app.groups[account] = {} 1793 1794 self_jid = str(app.connections[account].get_own_jid()) 1795 if account != app.ZEROCONF_ACC_NAME: 1796 array[self_jid] = {'name': app.nicks[account], 1797 'groups': ['self_contact'], 1798 'subscription': 'both', 1799 'ask': 'none'} 1800 1801 # .keys() is needed 1802 for jid in list(array.keys()): 1803 # Remove the contact in roster. It might has changed 1804 self.remove_contact(jid, account, force=True) 1805 # Remove old Contact instances 1806 app.contacts.remove_jid(account, jid, remove_meta=False) 1807 jids = jid.split('/') 1808 # get jid 1809 ji = jids[0] 1810 # get resource 1811 resource = '' 1812 if len(jids) > 1: 1813 resource = '/'.join(jids[1:]) 1814 # get name 1815 name = array[jid]['name'] or '' 1816 show = 'offline' # show is offline by default 1817 status = '' # no status message by default 1818 1819 if app.jid_is_transport(jid): 1820 array[jid]['groups'] = [_('Transports')] 1821 #TRANSP - potential 1822 contact1 = app.contacts.create_contact(jid=ji, account=account, 1823 name=name, groups=array[jid]['groups'], show=show, 1824 status=status, sub=array[jid]['subscription'], 1825 ask=array[jid]['ask'], resource=resource) 1826 app.contacts.add_contact(account, contact1) 1827 1828 # If we already have chat windows opened, update them with new 1829 # contact instance 1830 chat_control = app.interface.msg_win_mgr.get_control(ji, account) 1831 if chat_control: 1832 chat_control.contact = contact1 1833 1834 def connected_rooms(self, account): 1835 if account in list(app.gc_connected[account].values()): 1836 return True 1837 return False 1838 1839 def on_event_removed(self, event_list): 1840 """ 1841 Remove contacts on last events removed 1842 1843 Only performed if removal was requested before but the contact still had 1844 pending events 1845 """ 1846 1847 msg_log_ids = [] 1848 for ev in event_list: 1849 if ev.type_ != 'printed_chat': 1850 continue 1851 if ev.msg_log_id: 1852 # There is a msg_log_id 1853 msg_log_ids.append(ev.msg_log_id) 1854 1855 if msg_log_ids: 1856 app.storage.archive.set_read_messages(msg_log_ids) 1857 1858 contact_list = ((event.jid.split('/')[0], event.account) for event in \ 1859 event_list) 1860 1861 for jid, account in contact_list: 1862 self.draw_contact(jid, account) 1863 # Remove contacts in roster if removal was requested 1864 key = (jid, account) 1865 if key in list(self.contacts_to_be_removed.keys()): 1866 backend = self.contacts_to_be_removed[key]['backend'] 1867 del self.contacts_to_be_removed[key] 1868 # Remove contact will delay removal if there are more events 1869 # pending 1870 self.remove_contact(jid, account, backend=backend) 1871 self.show_title() 1872 1873 def open_event(self, account, jid, event): 1874 """ 1875 If an event was handled, return True, else return False 1876 """ 1877 ft = app.interface.instances['file_transfers'] 1878 event = app.events.get_first_event(account, jid, event.type_) 1879 if event.type_ == 'normal': 1880 SingleMessageWindow(account, jid, 1881 action='receive', from_whom=jid, subject=event.subject, 1882 message=event.message, resource=event.resource) 1883 app.events.remove_events(account, jid, event) 1884 return True 1885 1886 if event.type_ == 'file-request': 1887 contact = app.contacts.get_contact_with_highest_priority(account, 1888 jid) 1889 ft.show_file_request(account, contact, event.file_props) 1890 app.events.remove_events(account, jid, event) 1891 return True 1892 1893 if event.type_ in ('file-request-error', 'file-send-error'): 1894 ft.show_send_error(event.file_props) 1895 app.events.remove_events(account, jid, event) 1896 return True 1897 1898 if event.type_ in ('file-error', 'file-stopped'): 1899 msg_err = '' 1900 if event.file_props.error == -1: 1901 msg_err = _('Remote contact stopped transfer') 1902 elif event.file_props.error == -6: 1903 msg_err = _('Error opening file') 1904 ft.show_stopped(jid, event.file_props, error_msg=msg_err) 1905 app.events.remove_events(account, jid, event) 1906 return True 1907 1908 if event.type_ == 'file-hash-error': 1909 ft.show_hash_error(jid, event.file_props, account) 1910 app.events.remove_events(account, jid, event) 1911 return True 1912 1913 if event.type_ == 'file-completed': 1914 ft.show_completed(jid, event.file_props) 1915 app.events.remove_events(account, jid, event) 1916 return True 1917 1918 if event.type_ == 'gc-invitation': 1919 open_window('GroupChatInvitation', 1920 account=account, 1921 event=event) 1922 app.events.remove_events(account, jid, event) 1923 return True 1924 1925 if event.type_ == 'subscription_request': 1926 open_window('SubscriptionRequest', 1927 account=account, 1928 jid=jid, 1929 text=event.text, 1930 user_nick=event.nick) 1931 app.events.remove_events(account, jid, event) 1932 return True 1933 1934 if event.type_ == 'unsubscribed': 1935 app.interface.show_unsubscribed_dialog(account, event.contact) 1936 app.events.remove_events(account, jid, event) 1937 return True 1938 1939 if event.type_ == 'jingle-incoming': 1940 ctrl = app.interface.msg_win_mgr.get_control(jid, account) 1941 if ctrl: 1942 ctrl.parent_win.set_active_tab(ctrl) 1943 else: 1944 ctrl = app.interface.new_chat_from_jid(account, jid) 1945 ctrl.add_call_received_message(event) 1946 return True 1947 1948 return False 1949 1950################################################################################ 1951### This and that... random. 1952################################################################################ 1953 1954 def show_roster_vbox(self, active): 1955 vb = self.xml.get_object('roster_vbox2') 1956 if active: 1957 vb.set_no_show_all(False) 1958 vb.show() 1959 else: 1960 vb.hide() 1961 vb.set_no_show_all(True) 1962 1963 def authorize(self, widget, jid, account): 1964 """ 1965 Authorize a contact (by re-sending auth menuitem) 1966 """ 1967 app.connections[account].get_module('Presence').subscribed(jid) 1968 InformationDialog(_('Authorization sent'), 1969 _('"%s" will now see your status.') %jid) 1970 1971 def req_sub(self, widget, jid, txt, account, groups=None, nickname=None, 1972 auto_auth=False): 1973 """ 1974 Request subscription to a contact 1975 """ 1976 groups_list = groups or [] 1977 app.connections[account].get_module('Presence').subscribe( 1978 jid, txt, nickname, groups_list, auto_auth) 1979 contact = app.contacts.get_contact_with_highest_priority(account, jid) 1980 if not contact: 1981 contact = app.contacts.create_contact(jid=jid, account=account, 1982 name=nickname, groups=groups_list, show='requested', status='', 1983 ask='none', sub='subscribe') 1984 app.contacts.add_contact(account, contact) 1985 else: 1986 if not _('Not in contact list') in contact.get_shown_groups(): 1987 InformationDialog(_('Subscription request has been ' 1988 'sent'), _('If "%s" accepts this request you will know ' 1989 'their status.') % jid) 1990 return 1991 self.remove_contact(contact.jid, account, force=True) 1992 contact.groups = groups_list 1993 if nickname: 1994 contact.name = nickname 1995 self.add_contact(jid, account) 1996 1997 def revoke_auth(self, widget, jid, account): 1998 """ 1999 Revoke a contact's authorization 2000 """ 2001 app.connections[account].get_module('Presence').unsubscribed(jid) 2002 InformationDialog(_('Authorization removed'), 2003 _('Now "%s" will always see you as offline.') %jid) 2004 2005 def set_state(self, account, state): 2006 child_iterA = self._get_account_iter(account, self.model) 2007 if child_iterA: 2008 self.model[child_iterA][0] = get_icon_name(state) 2009 if app.interface.systray_enabled: 2010 app.interface.systray.change_status(state) 2011 2012 def set_connecting_state(self, account): 2013 self.set_state(account, 'connecting') 2014 2015 def send_status(self, account, status, txt): 2016 if status != 'offline': 2017 app.settings.set_account_setting(account, 'last_status', status) 2018 app.settings.set_account_setting(account, 'last_status_msg', 2019 helpers.to_one_line(txt)) 2020 if not app.account_is_available(account): 2021 self.set_connecting_state(account) 2022 2023 if status == 'offline': 2024 self.delete_pep(app.get_jid_from_account(account), account) 2025 2026 app.connections[account].change_status(status, txt) 2027 self._status_selector.update() 2028 2029 def delete_pep(self, jid, account): 2030 if jid == app.get_jid_from_account(account): 2031 app.connections[account].pep = {} 2032 self.draw_account(account) 2033 2034 for contact in app.contacts.get_contacts(account, jid): 2035 contact.pep = {} 2036 2037 self.draw_all_pep_types(jid, account) 2038 ctrl = app.interface.msg_win_mgr.get_control(jid, account) 2039 if ctrl: 2040 ctrl.update_all_pep_types() 2041 2042 def chg_contact_status(self, contact, show, status_message, account): 2043 """ 2044 When a contact changes their status 2045 """ 2046 contact_instances = app.contacts.get_contacts(account, contact.jid) 2047 contact.show = show 2048 contact.status = status_message 2049 # name is to show in conversation window 2050 name = contact.get_shown_name() 2051 fjid = contact.get_full_jid() 2052 2053 # The contact has several resources 2054 if len(contact_instances) > 1: 2055 if contact.resource != '': 2056 name += '/' + contact.resource 2057 2058 # Remove resource when going offline 2059 if show in ('offline', 'error') and \ 2060 not self.contact_has_pending_roster_events(contact, account): 2061 ctrl = app.interface.msg_win_mgr.get_control(fjid, account) 2062 if ctrl: 2063 ctrl.update_ui() 2064 ctrl.parent_win.redraw_tab(ctrl) 2065 # keep the contact around, since it's 2066 # already attached to the control 2067 else: 2068 app.contacts.remove_contact(account, contact) 2069 2070 elif contact.jid == app.get_jid_from_account(account) and \ 2071 show in ('offline', 'error'): 2072 self.remove_contact(contact.jid, account, backend=True) 2073 2074 uf_show = helpers.get_uf_show(show) 2075 2076 # print status in chat window and update status/GPG image 2077 ctrl = app.interface.msg_win_mgr.get_control(contact.jid, account) 2078 if ctrl and not ctrl.is_groupchat: 2079 ctrl.contact = app.contacts.get_contact_with_highest_priority( 2080 account, contact.jid) 2081 ctrl.update_status_display(name, uf_show, status_message) 2082 2083 if contact.resource: 2084 ctrl = app.interface.msg_win_mgr.get_control(fjid, account) 2085 if ctrl: 2086 ctrl.update_status_display(name, uf_show, status_message) 2087 2088 # Delete pep if needed 2089 keep_pep = any(c.show not in ('error', 'offline') for c in 2090 contact_instances) 2091 if not keep_pep and contact.jid != app.get_jid_from_account(account) \ 2092 and not contact.is_groupchat: 2093 self.delete_pep(contact.jid, account) 2094 2095 # Redraw everything and select the sender 2096 self.adjust_and_draw_contact_context(contact.jid, account) 2097 2098 2099 def on_status_changed(self, account, show): 2100 """ 2101 The core tells us that our status has changed 2102 """ 2103 if account not in app.contacts.get_accounts(): 2104 return 2105 child_iterA = self._get_account_iter(account, self.model) 2106 self_resource = app.connections[account].get_own_jid().resource 2107 self_contact = app.contacts.get_contact(account, 2108 app.get_jid_from_account(account), resource=self_resource) 2109 if self_contact: 2110 status_message = app.connections[account].status_message 2111 self.chg_contact_status(self_contact, show, status_message, account) 2112 self.set_account_status_icon(account) 2113 if show == 'offline': 2114 if self.quit_on_next_offline > -1: 2115 # we want to quit, we are waiting for all accounts to be offline 2116 self.quit_on_next_offline -= 1 2117 if self.quit_on_next_offline < 1: 2118 # all accounts offline, quit 2119 self.quit_gtkgui_interface() 2120 else: 2121 # No need to redraw contacts if we're quitting 2122 if child_iterA: 2123 self.model[child_iterA][Column.AVATAR_IMG] = None 2124 for jid in list(app.contacts.get_jid_list(account)): 2125 lcontact = app.contacts.get_contacts(account, jid) 2126 ctrl = app.interface.msg_win_mgr.get_gc_control(jid, 2127 account) 2128 for contact in [c for c in lcontact if ( 2129 (c.show != 'offline' or c.is_transport()) and not ctrl)]: 2130 self.chg_contact_status(contact, 'offline', '', account) 2131 if app.interface.systray_enabled: 2132 app.interface.systray.change_status(show) 2133 self._status_selector.update() 2134 2135 def change_status(self, _widget, account, status): 2136 app.interface.change_account_status(account, status=status) 2137 2138 def get_show(self, lcontact): 2139 prio = lcontact[0].priority 2140 show = lcontact[0].show 2141 for u in lcontact: 2142 if u.priority > prio: 2143 prio = u.priority 2144 show = u.show 2145 return show 2146 2147 def on_message_window_delete(self, win_mgr, msg_win): 2148 if app.settings.get('one_message_window') == 'always_with_roster': 2149 self.show_roster_vbox(True) 2150 resize_window(self.window, 2151 app.settings.get('roster_width'), 2152 app.settings.get('roster_height')) 2153 2154 def close_all_from_dict(self, dic): 2155 """ 2156 Close all the windows in the given dictionary 2157 """ 2158 for w in list(dic.values()): 2159 if isinstance(w, dict): 2160 self.close_all_from_dict(w) 2161 else: 2162 try: 2163 w.window.destroy() 2164 except (AttributeError, RuntimeError): 2165 w.destroy() 2166 2167 def close_all(self, account, force=False): 2168 """ 2169 Close all the windows from an account. If force is True, do not ask 2170 confirmation before closing chat/gc windows 2171 """ 2172 if account in app.interface.instances: 2173 self.close_all_from_dict(app.interface.instances[account]) 2174 for ctrl in app.interface.msg_win_mgr.get_controls(acct=account): 2175 ctrl.parent_win.remove_tab(ctrl, ctrl.parent_win.CLOSE_CLOSE_BUTTON, 2176 force=force) 2177 2178 def on_roster_window_delete_event(self, widget, event): 2179 """ 2180 Main window X button was clicked 2181 """ 2182 if not app.settings.get('quit_on_roster_x_button') and ( 2183 (app.interface.systray_enabled and app.settings.get('trayicon') != \ 2184 'on_event') or app.settings.get('allow_hide_roster')): 2185 save_roster_position(self.window) 2186 if os.name == 'nt' or app.settings.get('hide_on_roster_x_button'): 2187 self.window.hide() 2188 else: 2189 self.window.iconify() 2190 elif app.settings.get('quit_on_roster_x_button'): 2191 self.on_quit_request() 2192 else: 2193 def _on_ok(is_checked): 2194 if is_checked: 2195 app.settings.set('quit_on_roster_x_button', True) 2196 self.on_quit_request() 2197 ConfirmationCheckDialog( 2198 _('Quit Gajim'), 2199 _('You are about to quit Gajim'), 2200 _('Are you sure you want to quit Gajim?'), 2201 _('_Always quit when closing Gajim'), 2202 [DialogButton.make('Cancel'), 2203 DialogButton.make('Remove', 2204 text=_('_Quit'), 2205 callback=_on_ok)]).show() 2206 return True # Do NOT destroy the window 2207 2208 def prepare_quit(self): 2209 if self.save_done: 2210 return 2211 msgwin_width_adjust = 0 2212 2213 # in case show_roster_on_start is False and roster is never shown 2214 # window.window is None 2215 if self.window.get_window() is not None: 2216 save_roster_position(self.window) 2217 width, height = self.window.get_size() 2218 app.settings.set('roster_width', width) 2219 app.settings.set('roster_height', height) 2220 if not self.xml.get_object('roster_vbox2').get_property('visible'): 2221 # The roster vbox is hidden, so the message window is larger 2222 # then we want to save (i.e. the window will grow every startup) 2223 # so adjust. 2224 msgwin_width_adjust = -1 * width 2225 app.settings.set('last_roster_visible', 2226 self.window.get_property('visible')) 2227 app.interface.msg_win_mgr.save_opened_controls() 2228 app.interface.msg_win_mgr.shutdown(msgwin_width_adjust) 2229 2230 app.settings.set('collapsed_rows', '\t'.join(self.collapsed_rows)) 2231 app.interface.save_config() 2232 for account in app.connections: 2233 app.connections[account].quit(True) 2234 self.close_all(account) 2235 if app.interface.systray_enabled: 2236 app.interface.hide_systray() 2237 self.save_done = True 2238 2239 def quit_gtkgui_interface(self): 2240 """ 2241 When we quit the gtk interface - exit gtk 2242 """ 2243 self.prepare_quit() 2244 self.application.quit() 2245 2246 def on_quit_request(self, widget=None): 2247 """ 2248 User wants to quit. Check if he should be warned about messages pending. 2249 Terminate all sessions and send offline to all connected account. We do 2250 NOT really quit gajim here 2251 """ 2252 accounts = list(app.connections.keys()) 2253 get_msg = False 2254 for acct in accounts: 2255 if app.account_is_available(acct): 2256 get_msg = True 2257 break 2258 2259 def on_continue3(message): 2260 self.quit_on_next_offline = 0 2261 accounts_to_disconnect = [] 2262 for acct in accounts: 2263 if app.account_is_available(acct): 2264 self.quit_on_next_offline += 1 2265 accounts_to_disconnect.append(acct) 2266 2267 if not self.quit_on_next_offline: 2268 # all accounts offline, quit 2269 self.quit_gtkgui_interface() 2270 return 2271 2272 for acct in accounts_to_disconnect: 2273 self.send_status(acct, 'offline', message) 2274 2275 def on_continue2(message): 2276 if 'file_transfers' not in app.interface.instances: 2277 on_continue3(message) 2278 return 2279 # check if there is an active file transfer 2280 from gajim.common.modules.bytestream import is_transfer_active 2281 files_props = app.interface.instances['file_transfers'].\ 2282 files_props 2283 transfer_active = False 2284 for x in files_props: 2285 for y in files_props[x]: 2286 if is_transfer_active(files_props[x][y]): 2287 transfer_active = True 2288 break 2289 2290 if transfer_active: 2291 ConfirmationDialog( 2292 _('Stop File Transfers'), 2293 _('You still have running file transfers'), 2294 _('If you quit now, the file(s) being transferred will ' 2295 'be lost.\n' 2296 'Do you still want to quit?'), 2297 [DialogButton.make('Cancel'), 2298 DialogButton.make('Remove', 2299 text=_('_Quit'), 2300 callback=on_continue3, 2301 args=[message])]).show() 2302 return 2303 on_continue3(message) 2304 2305 def on_continue(message): 2306 if message is None: 2307 # user pressed Cancel to change status message dialog 2308 return 2309 # check if we have unread messages 2310 unread = app.events.get_nb_events() 2311 2312 for event in app.events.get_all_events(['printed_gc_msg']): 2313 contact = app.contacts.get_groupchat_contact(event.account, 2314 event.jid) 2315 if contact is None or not contact.can_notify(): 2316 unread -= 1 2317 2318 # check if we have recent messages 2319 recent = False 2320 for win in app.interface.msg_win_mgr.windows(): 2321 for ctrl in win.controls(): 2322 fjid = ctrl.get_full_jid() 2323 if fjid in app.last_message_time[ctrl.account]: 2324 if time.time() - app.last_message_time[ctrl.account][ 2325 fjid] < 2: 2326 recent = True 2327 break 2328 if recent: 2329 break 2330 2331 if unread or recent: 2332 ConfirmationDialog( 2333 _('Unread Messages'), 2334 _('You still have unread messages'), 2335 _('Messages will only be available for reading them later ' 2336 'if storing chat history is enabled and if the contact ' 2337 'is in your contact list.'), 2338 [DialogButton.make('Cancel'), 2339 DialogButton.make('Remove', 2340 text=_('_Quit'), 2341 callback=on_continue2, 2342 args=[message])]).show() 2343 return 2344 on_continue2(message) 2345 2346 if get_msg and ask_for_status_message('offline'): 2347 open_window('StatusChange', 2348 status='offline', 2349 callback=on_continue, 2350 show_pep=False) 2351 else: 2352 on_continue('') 2353 2354 def _nec_presence_received(self, obj): 2355 account = obj.conn.name 2356 jid = obj.jid 2357 2358 if obj.need_add_in_roster: 2359 self.add_contact(jid, account) 2360 2361 jid_list = app.contacts.get_jid_list(account) 2362 if jid in jid_list or jid == app.get_jid_from_account(account): 2363 if not app.jid_is_transport(jid) and len(obj.contact_list) == 1: 2364 if obj.old_show == 0 and obj.new_show > 1: 2365 GLib.timeout_add_seconds(5, self.remove_newly_added, jid, 2366 account) 2367 elif obj.old_show > 1 and obj.new_show == 0 and \ 2368 obj.conn.state.is_available: 2369 GLib.timeout_add_seconds(5, self.remove_to_be_removed, 2370 jid, account) 2371 2372 self.draw_contact(jid, account) 2373 2374 if app.jid_is_transport(jid) and jid in jid_list: 2375 # It must be an agent 2376 # Update existing iter and group counting 2377 self.draw_contact(jid, account) 2378 self.draw_group(_('Transports'), account) 2379 2380 if obj.contact: 2381 self.chg_contact_status(obj.contact, obj.show, obj.status, account) 2382 2383 if obj.popup: 2384 ctrl = app.interface.msg_win_mgr.search_control(jid, account) 2385 if ctrl: 2386 GLib.idle_add(ctrl.parent_win.set_active_tab, ctrl) 2387 else: 2388 ctrl = app.interface.new_chat(obj.contact, account) 2389 if app.events.get_events(account, obj.jid): 2390 ctrl.read_queue() 2391 2392 def _nec_roster_received(self, obj): 2393 if obj.received_from_server: 2394 self.fill_contacts_and_groups_dicts(obj.roster, obj.conn.name) 2395 self.add_account_contacts(obj.conn.name) 2396 self.fire_up_unread_messages_events(obj.conn.name) 2397 else: 2398 # add self contact 2399 account = obj.conn.name 2400 self_jid = app.get_jid_from_account(account) 2401 if self_jid not in app.contacts.get_jid_list(account): 2402 sha = app.settings.get_account_setting(account, 'avatar_sha') 2403 contact = app.contacts.create_contact( 2404 jid=self_jid, account=account, name=app.nicks[account], 2405 groups=['self_contact'], show='offline', sub='both', 2406 ask='none', avatar_sha=sha) 2407 app.contacts.add_contact(account, contact) 2408 self.add_contact(self_jid, account) 2409 2410 if app.settings.get('remember_opened_chat_controls'): 2411 account = obj.conn.name 2412 controls = app.settings.get_account_setting( 2413 account, 'opened_chat_controls') 2414 if controls: 2415 for jid in controls.split(','): 2416 contact = \ 2417 app.contacts.get_contact_with_highest_priority( 2418 account, jid) 2419 if not contact: 2420 contact = self.add_to_not_in_the_roster( 2421 account, jid) 2422 app.interface.on_open_chat_window( 2423 None, contact, account) 2424 app.settings.set_account_setting( 2425 account, 'opened_chat_controls', '') 2426 GLib.idle_add(self.refilter_shown_roster_items) 2427 2428 def _nec_anonymous_auth(self, obj): 2429 """ 2430 This event is raised when our JID changed (most probably because we use 2431 anonymous account. We update contact and roster entry in this case 2432 """ 2433 self.rename_self_contact(obj.old_jid, obj.new_jid, obj.conn.name) 2434 2435 def _nec_our_show(self, event): 2436 if event.show == 'offline': 2437 self.application.set_account_actions_state(event.account) 2438 self.application.update_app_actions_state() 2439 2440 self.on_status_changed(event.account, event.show) 2441 2442 def _nec_connection_type(self, obj): 2443 self.draw_account(obj.conn.name) 2444 2445 def _nec_agent_removed(self, obj): 2446 for jid in obj.jid_list: 2447 self.remove_contact(jid, obj.conn.name, backend=True) 2448 2449 def _on_mood_received(self, event): 2450 if event.is_self_message: 2451 self.draw_account(event.account) 2452 self._draw_pep(event.account, event.jid, PEPEventType.MOOD) 2453 2454 def _on_activity_received(self, event): 2455 if event.is_self_message: 2456 self.draw_account(event.account) 2457 self._draw_pep(event.account, event.jid, PEPEventType.ACTIVITY) 2458 2459 def _on_tune_received(self, event): 2460 if event.is_self_message: 2461 self.draw_account(event.account) 2462 self._draw_pep(event.account, event.jid, PEPEventType.TUNE) 2463 2464 def _on_location_received(self, event): 2465 if event.is_self_message: 2466 self.draw_account(event.account) 2467 self._draw_pep(event.account, event.jid, PEPEventType.LOCATION) 2468 2469 def _on_nickname_received(self, event): 2470 self.draw_contact(event.jid, event.account) 2471 2472 def _nec_update_avatar(self, obj): 2473 app.log('avatar').debug('Draw roster avatar: %s', obj.jid) 2474 self.draw_avatar(obj.jid, obj.account) 2475 2476 def _nec_muc_subject_received(self, event): 2477 self.draw_contact(event.room_jid, event.account) 2478 2479 def _on_muc_disco_update(self, event): 2480 self.draw_contact(str(event.room_jid), event.account) 2481 2482 def _on_bookmarks_received(self, event): 2483 con = app.connections[event.account] 2484 for bookmark in con.get_module('Bookmarks').bookmarks: 2485 self.draw_contact(str(bookmark.jid), event.account) 2486 2487 def _nec_metacontacts_received(self, obj): 2488 self.redraw_metacontacts(obj.conn.name) 2489 2490 def _nec_signed_in(self, obj): 2491 self.application.set_account_actions_state(obj.conn.name, True) 2492 self.application.update_app_actions_state() 2493 self.draw_account(obj.conn.name) 2494 2495 def _nec_decrypted_message_received(self, obj): 2496 if not obj.msgtxt: 2497 return True 2498 if obj.properties.type.value not in ('normal', 'chat'): 2499 return 2500 2501 if obj.popup and not obj.session.control: 2502 contact = app.contacts.get_contact(obj.conn.name, obj.jid) 2503 obj.session.control = app.interface.new_chat(contact, 2504 obj.conn.name, session=obj.session) 2505 if app.events.get_events(obj.conn.name, obj.fjid): 2506 obj.session.control.read_queue() 2507 2508 if not obj.properties.is_muc_pm and obj.show_in_roster: 2509 self.draw_contact(obj.jid, obj.conn.name) 2510 self.show_title() # we show the * or [n] 2511 # Select the big brother contact in roster, it's visible because it 2512 # has events. 2513 family = app.contacts.get_metacontacts_family(obj.conn.name, 2514 obj.jid) 2515 if family: 2516 _nearby_family, bb_jid, bb_account = \ 2517 app.contacts.get_nearby_family_and_big_brother(family, 2518 obj.conn.name) 2519 else: 2520 bb_jid, bb_account = obj.jid, obj.conn.name 2521 self.select_contact(bb_jid, bb_account) 2522 2523################################################################################ 2524### Menu and GUI callbacks 2525### FIXME: order callbacks in itself... 2526################################################################################ 2527 2528 def on_info(self, widget, contact, account): 2529 """ 2530 Call vcard_information_window class to display contact's information 2531 """ 2532 if app.connections[account].is_zeroconf: 2533 self.on_info_zeroconf(widget, contact, account) 2534 return 2535 2536 info = app.interface.instances[account]['infos'] 2537 if contact.jid in info: 2538 info[contact.jid].window.present() 2539 else: 2540 info[contact.jid] = vcard.VcardWindow(contact, account) 2541 2542 def on_info_zeroconf(self, widget, contact, account): 2543 info = app.interface.instances[account]['infos'] 2544 if contact.jid in info: 2545 info[contact.jid].window.present() 2546 else: 2547 contact = app.contacts.get_first_contact_from_jid(account, 2548 contact.jid) 2549 if contact.show in ('offline', 'error'): 2550 # don't show info on offline contacts 2551 return 2552 info[contact.jid] = vcard.ZeroconfVcardWindow(contact, account) 2553 2554 def on_edit_agent(self, widget, contact, account): 2555 """ 2556 When we want to modify the agent registration 2557 """ 2558 ServiceRegistration(account, contact.jid) 2559 2560 def on_remove_agent(self, widget, list_): 2561 """ 2562 When an agent is requested to be removed. list_ is a list of (contact, 2563 account) tuple 2564 """ 2565 for (contact, account) in list_: 2566 if app.settings.get_account_setting(account, 'hostname') == \ 2567 contact.jid: 2568 # We remove the server contact 2569 # remove it from treeview 2570 app.connections[account].get_module('Presence').unsubscribe(contact.jid) 2571 self.remove_contact(contact.jid, account, backend=True) 2572 return 2573 2574 def remove(): 2575 for (contact, account) in list_: 2576 full_jid = contact.get_full_jid() 2577 app.connections[account].get_module('Gateway').unsubscribe(full_jid) 2578 # remove transport from treeview 2579 self.remove_contact(contact.jid, account, backend=True) 2580 2581 # Check if there are unread events from some contacts 2582 has_unread_events = False 2583 for (contact, account) in list_: 2584 for jid in app.events.get_events(account): 2585 if jid.endswith(contact.jid): 2586 has_unread_events = True 2587 break 2588 if has_unread_events: 2589 ErrorDialog( 2590 _('You have unread messages'), 2591 _('You must read them before removing this transport.')) 2592 return 2593 if len(list_) == 1: 2594 pritext = _('Transport \'%s\' will be removed') % list_[0][0].jid 2595 sectext = _('You will no longer be able to send and receive ' 2596 'messages from and to contacts using this transport.') 2597 else: 2598 pritext = _('Transports will be removed') 2599 jids = '' 2600 for (contact, account) in list_: 2601 jids += '\n ' + contact.get_shown_name() + ',' 2602 jids = jids[:-1] + '.' 2603 sectext = _('You will no longer be able to send and receive ' 2604 'messages from and to contacts using these ' 2605 'transports:\n%s') % jids 2606 ConfirmationDialog( 2607 _('Remove Transport'), 2608 pritext, 2609 sectext, 2610 [DialogButton.make('Cancel'), 2611 DialogButton.make('Remove', 2612 callback=remove)], 2613 transient_for=self.window).show() 2614 2615 def _nec_blocking(self, obj): 2616 for jid in obj.changed: 2617 self.draw_contact(str(jid), obj.conn.name) 2618 2619 def on_block(self, widget, list_): 2620 """ 2621 When clicked on the 'block' button in context menu. list_ is a list of 2622 (contact, account) 2623 """ 2624 def _block_it(is_checked=None, report=None): 2625 if is_checked is not None: # Dialog has been shown 2626 if is_checked: 2627 app.settings.set('confirm_block', 'no') 2628 else: 2629 app.settings.set('confirm_block', 'yes') 2630 2631 accounts = [] 2632 for _, account in list_: 2633 con = app.connections[account] 2634 if con.get_module('Blocking').supported: 2635 accounts.append(account) 2636 2637 for acct in accounts: 2638 l_ = [i[0] for i in list_ if i[1] == acct] 2639 con = app.connections[acct] 2640 jid_list = [contact.jid for contact in l_] 2641 con.get_module('Blocking').block(jid_list, report) 2642 for contact in l_: 2643 app.events.remove_events(acct, contact.jid) 2644 ctrl = app.interface.msg_win_mgr.get_control( 2645 contact.jid, acct) 2646 if ctrl: 2647 ctrl.parent_win.remove_tab( 2648 ctrl, ctrl.parent_win.CLOSE_COMMAND, force=True) 2649 if contact.show == 'not in roster': 2650 self.remove_contact(contact.jid, acct, force=True, 2651 backend=True) 2652 return 2653 self.draw_contact(contact.jid, acct) 2654 2655 # Check if confirmation is needed for blocking 2656 confirm_block = app.settings.get('confirm_block') 2657 if confirm_block == 'no': 2658 _block_it() 2659 return 2660 2661 ConfirmationCheckDialog( 2662 _('Block Contact'), 2663 _('Really block this contact?'), 2664 _('You will appear offline for this contact and you ' 2665 'will not receive further messages.'), 2666 _('_Do not ask again'), 2667 [DialogButton.make('Cancel'), 2668 DialogButton.make('OK', 2669 text=_('_Report Spam'), 2670 callback=_block_it, 2671 kwargs={'report': 'spam'}), 2672 DialogButton.make('Remove', 2673 text=_('_Block'), 2674 callback=_block_it)], 2675 modal=False).show() 2676 2677 def on_unblock(self, widget, list_): 2678 """ 2679 When clicked on the 'unblock' button in context menu. 2680 """ 2681 accounts = [] 2682 for _, account in list_: 2683 con = app.connections[account] 2684 if con.get_module('Blocking').supported: 2685 accounts.append(account) 2686 2687 for acct in accounts: 2688 l_ = [i[0] for i in list_ if i[1] == acct] 2689 con = app.connections[acct] 2690 jid_list = [contact.jid for contact in l_] 2691 con.get_module('Blocking').unblock(jid_list) 2692 for contact in l_: 2693 self.draw_contact(contact.jid, acct) 2694 2695 def on_rename(self, widget, row_type, jid, account): 2696 # This function is called either by F2 or by Rename menuitem 2697 if 'rename' in app.interface.instances: 2698 app.interface.instances['rename'].dialog.present() 2699 return 2700 2701 # Account is offline, don't allow to rename 2702 if not app.account_is_available(account): 2703 return 2704 if row_type in ('contact', 'agent'): 2705 # It's jid 2706 title = _('Rename Contact') 2707 text = _('Rename contact %s?') % jid 2708 sec_text = _('Please enter a new nickname') 2709 old_text = app.contacts.get_contact_with_highest_priority(account, 2710 jid).name 2711 elif row_type == 'group': 2712 if jid in helpers.special_groups + (_('General'),): 2713 return 2714 old_text = jid 2715 title = _('Rename Group') 2716 text = _('Rename group %s?') % GLib.markup_escape_text(jid) 2717 sec_text = _('Please enter a new name') 2718 2719 def _on_renamed(new_text, account, row_type, jid, old_text): 2720 if row_type in ('contact', 'agent'): 2721 if old_text == new_text: 2722 return 2723 contacts = app.contacts.get_contacts(account, jid) 2724 for contact in contacts: 2725 contact.name = new_text 2726 con = app.connections[account] 2727 con.get_module('Roster').update_contact( 2728 jid, new_text, contacts[0].groups) 2729 self.draw_contact(jid, account) 2730 # Update opened chats 2731 for ctrl in app.interface.msg_win_mgr.get_controls(jid, 2732 account): 2733 ctrl.update_ui() 2734 win = app.interface.msg_win_mgr.get_window(jid, account) 2735 win.redraw_tab(ctrl) 2736 win.show_title() 2737 elif row_type == 'group': 2738 # In Column.JID column, we hold the group name (which is not escaped) 2739 self.rename_group(old_text, new_text, account) 2740 2741 InputDialog( 2742 title, 2743 text, 2744 sec_text, 2745 [DialogButton.make('Cancel'), 2746 DialogButton.make('Accept', 2747 text=_('_Rename'), 2748 callback=_on_renamed, 2749 args=[account, 2750 row_type, 2751 jid, 2752 old_text])], 2753 input_str=old_text, 2754 transient_for=self.window).show() 2755 2756 def on_remove_group_item_activated(self, widget, group, account): 2757 def _on_ok(is_checked): 2758 for contact in app.contacts.get_contacts_from_group(account, 2759 group): 2760 if not is_checked: 2761 self.remove_contact_from_groups(contact.jid, account, 2762 [group]) 2763 else: 2764 app.connections[account].get_module( 2765 'Presence').unsubscribe(contact.jid) 2766 self.remove_contact(contact.jid, account, backend=True) 2767 2768 ConfirmationCheckDialog( 2769 _('Remove Group'), 2770 _('Remove Group'), 2771 _('Do you want to remove %s from the contact list?') % group, 2772 _('_Also remove all contacts of this group from contact list'), 2773 [DialogButton.make('Cancel'), 2774 DialogButton.make('Remove', 2775 callback=_on_ok)]).show() 2776 2777 def on_edit_groups(self, widget, list_): 2778 dialogs.EditGroupsDialog(list_) 2779 2780 def on_disconnect(self, widget, jid, account): 2781 """ 2782 When disconnect menuitem is activated: disconnect from room 2783 """ 2784 if jid in app.interface.minimized_controls[account]: 2785 ctrl = app.interface.minimized_controls[account][jid] 2786 ctrl.leave() 2787 self.remove_groupchat(jid, account) 2788 2789 def on_send_single_message_menuitem_activate(self, widget, account, 2790 contact=None): 2791 if contact is None: 2792 SingleMessageWindow(account, action='send') 2793 elif isinstance(contact, list): 2794 SingleMessageWindow(account, contact, 'send') 2795 else: 2796 jid = contact.jid 2797 if contact.jid == app.get_jid_from_account(account): 2798 jid += '/' + contact.resource 2799 SingleMessageWindow(account, jid, 'send') 2800 2801 def on_send_file_menuitem_activate(self, widget, contact, account, 2802 resource=None): 2803 app.interface.instances['file_transfers'].show_file_send_request( 2804 account, contact) 2805 2806 def on_invite_to_room(self, 2807 _widget, 2808 list_, 2809 room_jid, 2810 room_account, 2811 resource=None): 2812 """ 2813 Resource parameter MUST NOT be used if more than one contact in list 2814 """ 2815 gc_control = app.get_groupchat_control(room_account, room_jid) 2816 if gc_control is None: 2817 return 2818 2819 for contact, _ in list_: 2820 contact_jid = contact.jid 2821 if resource: # we MUST have one contact only in list_ 2822 contact_jid += '/' + resource 2823 gc_control.invite(contact_jid) 2824 2825 def on_all_groupchat_maximized(self, widget, group_list): 2826 for (contact, account) in group_list: 2827 self.on_groupchat_maximized(widget, contact.jid, account) 2828 2829 def on_groupchat_maximized(self, widget, jid, account): 2830 """ 2831 When a groupchat is maximized 2832 """ 2833 if not jid in app.interface.minimized_controls[account]: 2834 # Already opened? 2835 gc_control = app.interface.msg_win_mgr.get_gc_control(jid, 2836 account) 2837 if gc_control: 2838 mw = app.interface.msg_win_mgr.get_window(jid, account) 2839 mw.set_active_tab(gc_control) 2840 return 2841 ctrl = app.interface.minimized_controls[account][jid] 2842 mw = app.interface.msg_win_mgr.get_window(jid, account) 2843 if not mw: 2844 mw = app.interface.msg_win_mgr.create_window( 2845 ctrl.contact, ctrl.account, ctrl.type) 2846 id_ = mw.window.connect('motion-notify-event', 2847 ctrl._on_window_motion_notify) 2848 ctrl.handlers[id_] = mw.window 2849 ctrl.parent_win = mw 2850 ctrl.on_groupchat_maximize() 2851 mw.new_tab(ctrl) 2852 mw.set_active_tab(ctrl) 2853 self.remove_groupchat(jid, account, maximize=True) 2854 2855 def on_groupchat_rename(self, _widget, jid, account): 2856 def _on_rename(new_name): 2857 con = app.connections[account] 2858 con.get_module('Bookmarks').modify(jid, name=new_name) 2859 2860 contact = app.contacts.get_first_contact_from_jid(account, jid) 2861 name = contact.get_shown_name() 2862 2863 InputDialog( 2864 _('Rename Group Chat'), 2865 _('Rename Group Chat'), 2866 _('Please enter a new name for this group chat'), 2867 [DialogButton.make('Cancel'), 2868 DialogButton.make('Accept', 2869 text=_('_Rename'), 2870 callback=_on_rename)], 2871 input_str=name, 2872 transient_for=self.window).show() 2873 2874 def on_change_status_message_activate(self, _widget, account): 2875 app.interface.change_account_status(account) 2876 2877 def on_add_to_roster(self, widget, contact, account): 2878 AddNewContactWindow(account, contact.jid, contact.name) 2879 2880 def on_roster_treeview_key_press_event(self, widget, event): 2881 """ 2882 When a key is pressed in the treeviews 2883 """ 2884 if event.keyval == Gdk.KEY_Escape: 2885 if self.rfilter_enabled: 2886 self.disable_rfilter() 2887 else: 2888 self.tree.get_selection().unselect_all() 2889 elif event.keyval == Gdk.KEY_F2: 2890 treeselection = self.tree.get_selection() 2891 model, list_of_paths = treeselection.get_selected_rows() 2892 if len(list_of_paths) != 1: 2893 return 2894 path = list_of_paths[0] 2895 type_ = model[path][Column.TYPE] 2896 if type_ in ('contact', 'group', 'agent'): 2897 jid = model[path][Column.JID] 2898 account = model[path][Column.ACCOUNT] 2899 self.on_rename(widget, type_, jid, account) 2900 2901 elif event.keyval == Gdk.KEY_Delete: 2902 treeselection = self.tree.get_selection() 2903 model, list_of_paths = treeselection.get_selected_rows() 2904 if not list_of_paths: 2905 return 2906 type_ = model[list_of_paths[0]][Column.TYPE] 2907 account = model[list_of_paths[0]][Column.ACCOUNT] 2908 if type_ in ('account', 'group', 'self_contact') or \ 2909 account == app.ZEROCONF_ACC_NAME: 2910 return 2911 list_ = [] 2912 for path in list_of_paths: 2913 if model[path][Column.TYPE] != type_: 2914 return 2915 jid = model[path][Column.JID] 2916 account = model[path][Column.ACCOUNT] 2917 if not app.account_is_available(account): 2918 continue 2919 contact = app.contacts.get_contact_with_highest_priority( 2920 account, jid) 2921 list_.append((contact, account)) 2922 if not list_: 2923 return 2924 if type_ == 'contact': 2925 self.on_req_usub(widget, list_) 2926 elif type_ == 'agent': 2927 self.on_remove_agent(widget, list_) 2928 2929 elif not (event.get_state() & 2930 (Gdk.ModifierType.CONTROL_MASK | 2931 Gdk.ModifierType.MOD1_MASK)): 2932 num = Gdk.keyval_to_unicode(event.keyval) 2933 if num and num > 31: 2934 # if we got unicode symbol without ctrl / alt 2935 self.enable_rfilter(chr(num)) 2936 2937 elif event.get_state() & Gdk.ModifierType.CONTROL_MASK and \ 2938 event.get_state() & Gdk.ModifierType.SHIFT_MASK and \ 2939 event.keyval == Gdk.KEY_U: 2940 self.enable_rfilter('') 2941 self.rfilter_entry.event(event) 2942 2943 elif event.keyval == Gdk.KEY_Left: 2944 treeselection = self.tree.get_selection() 2945 model, list_of_paths = treeselection.get_selected_rows() 2946 if len(list_of_paths) != 1: 2947 return 2948 path = list_of_paths[0] 2949 iter_ = model.get_iter(path) 2950 if model.iter_has_child(iter_) and self.tree.row_expanded(path): 2951 self.tree.collapse_row(path) 2952 return True 2953 if path.get_depth() > 1: 2954 self.tree.set_cursor(path[:-1]) 2955 return True 2956 elif event.keyval == Gdk.KEY_Right: 2957 treeselection = self.tree.get_selection() 2958 model, list_of_paths = treeselection.get_selected_rows() 2959 if len(list_of_paths) != 1: 2960 return 2961 path = list_of_paths[0] 2962 iter_ = model.get_iter(path) 2963 if model.iter_has_child(iter_): 2964 self.tree.expand_row(path, False) 2965 return True 2966 2967 def accel_group_func(self, accel_group, acceleratable, keyval, modifier): 2968 # CTRL mask 2969 if modifier & Gdk.ModifierType.CONTROL_MASK: 2970 if keyval == Gdk.KEY_s: # CTRL + s 2971 app.interface.change_status() 2972 return True 2973 if keyval == Gdk.KEY_k: # CTRL + k 2974 self.enable_rfilter('') 2975 2976 def on_roster_treeview_button_press_event(self, widget, event): 2977 try: 2978 pos = self.tree.get_path_at_pos(int(event.x), int(event.y)) 2979 path, x = pos[0], pos[2] 2980 except TypeError: 2981 self.tree.get_selection().unselect_all() 2982 return False 2983 2984 if event.button == 3: # Right click 2985 try: 2986 model, list_of_paths = self.tree.get_selection().\ 2987 get_selected_rows() 2988 except TypeError: 2989 list_of_paths = [] 2990 if path not in list_of_paths: 2991 self.tree.get_selection().unselect_all() 2992 self.tree.get_selection().select_path(path) 2993 return self.show_treeview_menu(event) 2994 2995 if event.button == 2: # Middle click 2996 try: 2997 model, list_of_paths = self.tree.get_selection().\ 2998 get_selected_rows() 2999 except TypeError: 3000 list_of_paths = [] 3001 if list_of_paths != [path]: 3002 self.tree.get_selection().unselect_all() 3003 self.tree.get_selection().select_path(path) 3004 type_ = model[path][Column.TYPE] 3005 if type_ in ('agent', 'contact', 'self_contact', 'groupchat'): 3006 self.on_row_activated(widget, path) 3007 elif type_ == 'account': 3008 account = model[path][Column.ACCOUNT] 3009 if account != 'all': 3010 if app.account_is_available(account): 3011 app.interface.change_account_status(account) 3012 return True 3013 3014 show = helpers.get_global_show() 3015 if show == 'offline': 3016 return True 3017 app.interface.change_status() 3018 return True 3019 3020 if event.button == 1: # Left click 3021 model = self.modelfilter 3022 type_ = model[path][Column.TYPE] 3023 # x_min is the x start position of status icon column 3024 if app.settings.get('avatar_position_in_roster') == 'left': 3025 x_min = AvatarSize.ROSTER 3026 else: 3027 x_min = 0 3028 3029 if type_ == 'group' and x < 27: 3030 # first cell in 1st column (the arrow SINGLE clicked) 3031 if self.tree.row_expanded(path): 3032 self.tree.collapse_row(path) 3033 else: 3034 self.expand_group_row(path) 3035 3036 elif type_ == 'contact' and x_min < x < x_min + 27: 3037 if self.tree.row_expanded(path): 3038 self.tree.collapse_row(path) 3039 else: 3040 self.tree.expand_row(path, False) 3041 3042 def expand_group_row(self, path): 3043 self.tree.expand_row(path, False) 3044 iter_ = self.modelfilter.get_iter(path) 3045 child_iter = self.modelfilter.iter_children(iter_) 3046 while child_iter: 3047 type_ = self.modelfilter[child_iter][Column.TYPE] 3048 account = self.modelfilter[child_iter][Column.ACCOUNT] 3049 group = self.modelfilter[child_iter][Column.JID] 3050 if type_ == 'group' and account + group not in self.collapsed_rows: 3051 self.expand_group_row(self.modelfilter.get_path(child_iter)) 3052 child_iter = self.modelfilter.iter_next(child_iter) 3053 3054 def on_req_usub(self, widget, list_): 3055 """ 3056 Remove a contact. list_ is a list of (contact, account) tuples 3057 """ 3058 def on_ok(is_checked): 3059 remove_auth = True 3060 if len(list_) == 1: 3061 contact = list_[0][0] 3062 if contact.sub != 'to' and is_checked: 3063 remove_auth = False 3064 for (contact, account) in list_: 3065 if _('Not in contact list') not in contact.get_shown_groups(): 3066 app.connections[account].get_module('Presence').unsubscribe(contact.jid, 3067 remove_auth) 3068 self.remove_contact(contact.jid, account, backend=True) 3069 if not remove_auth and contact.sub == 'both': 3070 contact.name = '' 3071 contact.groups = [] 3072 contact.sub = 'from' 3073 # we can't see him, but have to set it manually in contact 3074 contact.show = 'offline' 3075 app.contacts.add_contact(account, contact) 3076 self.add_contact(contact.jid, account) 3077 def on_ok2(): 3078 on_ok(False) 3079 3080 if len(list_) == 1: 3081 contact = list_[0][0] 3082 title = _('Remove Contact') 3083 pritext = _('Remove contact from contact list') 3084 sectext = _('You are about to remove %(name)s (%(jid)s) from ' 3085 'your contact list.\n') % { 3086 'name': contact.get_shown_name(), 3087 'jid': contact.jid} 3088 if contact.sub == 'to': 3089 ConfirmationDialog( 3090 title, 3091 pritext, 3092 sectext + \ 3093 _('By removing this contact you also remove authorization. ' 3094 'This means the contact will see you as offline.'), 3095 [DialogButton.make('Cancel'), 3096 DialogButton.make('Remove', 3097 callback=on_ok2)]).show() 3098 elif _('Not in contact list') in contact.get_shown_groups(): 3099 # Contact is not in roster 3100 ConfirmationDialog( 3101 title, 3102 pritext, 3103 sectext + \ 3104 _('Do you want to continue?'), 3105 [DialogButton.make('Cancel'), 3106 DialogButton.make('Remove', 3107 callback=on_ok2)]).show() 3108 else: 3109 ConfirmationCheckDialog( 3110 title, 3111 pritext, 3112 sectext + \ 3113 _('By removing this contact you also remove authorization. ' 3114 'This means the contact will see you as offline.'), 3115 _('_I want this contact to know my status after removal'), 3116 [DialogButton.make('Cancel'), 3117 DialogButton.make('Remove', 3118 callback=on_ok)], 3119 modal=False).show() 3120 else: 3121 # several contact to remove at the same time 3122 pritext = _('Remove contacts from contact list') 3123 jids = '' 3124 for contact, _account in list_: 3125 jids += '%(name)s (%(jid)s)\n' % { 3126 'name': contact.get_shown_name(), 3127 'jid': contact.jid} 3128 sectext = _('By removing the following contacts, you will also ' 3129 'remove authorization. This means they will see you ' 3130 'as offline:\n\n%s') % jids 3131 ConfirmationDialog( 3132 _('Remove Contacts'), 3133 pritext, 3134 sectext, 3135 [DialogButton.make('Cancel'), 3136 DialogButton.make('Remove', 3137 callback=on_ok2)]).show() 3138 3139 def on_publish_tune_toggled(self, widget, account): 3140 active = widget.get_active() 3141 client = app.get_client(account) 3142 client.get_module('UserTune').set_enabled(active) 3143 3144 def on_publish_location_toggled(self, widget, account): 3145 active = widget.get_active() 3146 client = app.get_client(account) 3147 app.settings.set_account_setting(account, 'publish_location', active) 3148 3149 if active: 3150 location.enable() 3151 else: 3152 client = app.get_client(account) 3153 client.set_user_location(None) 3154 3155 client.get_module('Caps').update_caps() 3156 3157 def on_add_new_contact(self, widget, account): 3158 AddNewContactWindow(account) 3159 3160 def on_create_gc_activate(self, widget, account): 3161 """ 3162 When the create gc menuitem is clicked, show the create gc window 3163 """ 3164 app.app.activate_action('create-groupchat', 3165 GLib.Variant('s', account)) 3166 3167 def on_show_transports_action(self, action, param): 3168 app.settings.set('show_transports_group', param.get_boolean()) 3169 action.set_state(param) 3170 self.refilter_shown_roster_items() 3171 3172 def on_execute_command(self, widget, contact, account, resource=None): 3173 """ 3174 Execute command. Full JID needed; if it is other contact, resource is 3175 necessary. Widget is unnecessary, only to be able to make this a 3176 callback 3177 """ 3178 jid = contact.jid 3179 if resource: 3180 jid = jid + '/' + resource 3181 AdHocCommand(account, jid) 3182 3183 def on_view_server_info(self, _widget, account): 3184 app.app.activate_action('%s-server-info' % account, 3185 GLib.Variant('s', account)) 3186 3187 def on_roster_window_focus_in_event(self, widget, event): 3188 # roster received focus, so if we had urgency REMOVE IT 3189 # NOTE: we do not have to read the message to remove urgency 3190 # so this functions does that 3191 set_urgency_hint(widget, False) 3192 3193 # if a contact row is selected, update colors (eg. for status msg) 3194 # because gtk engines may differ in bg when window is selected 3195 # or not 3196 if self._last_selected_contact: 3197 for (jid, account) in self._last_selected_contact: 3198 self.draw_contact(jid, account, selected=True, focus=True) 3199 3200 def on_roster_window_focus_out_event(self, widget, event): 3201 # if a contact row is selected, update colors (eg. for status msg) 3202 # because gtk engines may differ in bg when window is selected 3203 # or not 3204 if self._last_selected_contact: 3205 for (jid, account) in self._last_selected_contact: 3206 self.draw_contact(jid, account, selected=True, focus=False) 3207 3208 def on_roster_window_key_press_event(self, widget, event): 3209 if event.keyval == Gdk.KEY_Escape: 3210 if self.rfilter_enabled: 3211 self.disable_rfilter() 3212 return True 3213 if app.interface.msg_win_mgr.mode == \ 3214 MessageWindowMgr.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER and \ 3215 app.interface.msg_win_mgr.one_window_opened(): 3216 # let message window close the tab 3217 return 3218 list_of_paths = self.tree.get_selection().get_selected_rows()[1] 3219 if not list_of_paths and not app.settings.get( 3220 'quit_on_roster_x_button') and ((app.interface.systray_enabled and\ 3221 app.settings.get('trayicon') == 'always') or app.settings.get( 3222 'allow_hide_roster')): 3223 if os.name == 'nt' or app.settings.get('hide_on_roster_x_button'): 3224 self.window.hide() 3225 else: 3226 self.window.iconify() 3227 elif event.get_state() & Gdk.ModifierType.CONTROL_MASK and event.keyval == \ 3228 Gdk.KEY_i: 3229 treeselection = self.tree.get_selection() 3230 model, list_of_paths = treeselection.get_selected_rows() 3231 for path in list_of_paths: 3232 type_ = model[path][Column.TYPE] 3233 if type_ in ('contact', 'agent'): 3234 jid = model[path][Column.JID] 3235 account = model[path][Column.ACCOUNT] 3236 contact = app.contacts.get_first_contact_from_jid(account, 3237 jid) 3238 self.on_info(widget, contact, account) 3239 elif event.get_state() & Gdk.ModifierType.CONTROL_MASK and event.keyval == \ 3240 Gdk.KEY_h: 3241 if app.settings.get('one_message_window') == 'always_with_roster': 3242 # Let MessageWindow handle this 3243 return 3244 treeselection = self.tree.get_selection() 3245 model, list_of_paths = treeselection.get_selected_rows() 3246 if len(list_of_paths) != 1: 3247 return 3248 path = list_of_paths[0] 3249 type_ = model[path][Column.TYPE] 3250 if type_ in ('contact', 'agent'): 3251 jid = model[path][Column.JID] 3252 account = model[path][Column.ACCOUNT] 3253 contact = app.contacts.get_first_contact_from_jid(account, 3254 jid) 3255 dict_ = {'jid': GLib.Variant('s', jid), 3256 'account': GLib.Variant('s', account)} 3257 app.app.activate_action('browse-history', 3258 GLib.Variant('a{sv}', dict_)) 3259 3260 def on_roster_window_popup_menu(self, widget): 3261 event = Gdk.Event.new(Gdk.EventType.KEY_PRESS) 3262 self.show_treeview_menu(event) 3263 3264 def on_row_activated(self, widget, path): 3265 """ 3266 When an iter is activated (double-click or single click if gnome is set 3267 this way) 3268 """ 3269 model = self.modelfilter 3270 account = model[path][Column.ACCOUNT] 3271 type_ = model[path][Column.TYPE] 3272 if type_ in ('group', 'account'): 3273 if self.tree.row_expanded(path): 3274 self.tree.collapse_row(path) 3275 else: 3276 self.tree.expand_row(path, False) 3277 return 3278 if self.rfilter_enabled: 3279 GObject.idle_add(self.disable_rfilter) 3280 jid = model[path][Column.JID] 3281 resource = None 3282 contact = app.contacts.get_contact_with_highest_priority(account, jid) 3283 titer = model.get_iter(path) 3284 if contact.is_groupchat: 3285 first_ev = app.events.get_first_event(account, jid) 3286 if first_ev and self.open_event(account, jid, first_ev): 3287 # We are invited to a GC 3288 # open event cares about connecting to it 3289 self.remove_groupchat(jid, account) 3290 else: 3291 self.on_groupchat_maximized(None, jid, account) 3292 return 3293 3294 # else 3295 first_ev = app.events.get_first_event(account, jid) 3296 if not first_ev: 3297 # look in other resources 3298 for c in app.contacts.get_contacts(account, jid): 3299 fjid = c.get_full_jid() 3300 first_ev = app.events.get_first_event(account, fjid) 3301 if first_ev: 3302 resource = c.resource 3303 break 3304 if not first_ev and model.iter_has_child(titer): 3305 child_iter = model.iter_children(titer) 3306 while not first_ev and child_iter: 3307 child_jid = model[child_iter][Column.JID] 3308 first_ev = app.events.get_first_event(account, child_jid) 3309 if first_ev: 3310 jid = child_jid 3311 else: 3312 child_iter = model.iter_next(child_iter) 3313 session = None 3314 if first_ev: 3315 if first_ev.type_ in ('chat', 'normal'): 3316 session = first_ev.session 3317 fjid = jid 3318 if resource: 3319 fjid += '/' + resource 3320 if self.open_event(account, fjid, first_ev): 3321 return 3322 # else 3323 contact = app.contacts.get_contact(account, jid, resource) 3324 if not contact or isinstance(contact, list): 3325 contact = app.contacts.get_contact_with_highest_priority(account, 3326 jid) 3327 if jid == app.get_jid_from_account(account): 3328 resource = None 3329 3330 app.interface.on_open_chat_window(None, contact, account, \ 3331 resource=resource, session=session) 3332 3333 def on_roster_treeview_row_activated(self, widget, path, col=0): 3334 """ 3335 When an iter is double clicked: open the first event window 3336 """ 3337 self.on_row_activated(widget, path) 3338 3339 def on_roster_treeview_row_expanded(self, widget, titer, path): 3340 """ 3341 When a row is expanded change the icon of the arrow 3342 """ 3343 self._toggeling_row = True 3344 model = widget.get_model() 3345 child_model = model.get_model() 3346 child_iter = model.convert_iter_to_child_iter(titer) 3347 3348 if self.regroup: # merged accounts 3349 accounts = list(app.connections.keys()) 3350 else: 3351 accounts = [model[titer][Column.ACCOUNT]] 3352 3353 type_ = model[titer][Column.TYPE] 3354 if type_ == 'group': 3355 group = model[titer][Column.JID] 3356 child_model[child_iter][Column.IMG] = get_icon_name('opened') 3357 if self.rfilter_enabled: 3358 return 3359 for account in accounts: 3360 if group in app.groups[account]: # This account has this group 3361 app.groups[account][group]['expand'] = True 3362 if account + group in self.collapsed_rows: 3363 self.collapsed_rows.remove(account + group) 3364 for contact in app.contacts.iter_contacts(account): 3365 jid = contact.jid 3366 if group in contact.groups and \ 3367 app.contacts.is_big_brother(account, jid, accounts) and \ 3368 account + group + jid not in self.collapsed_rows: 3369 titers = self._get_contact_iter(jid, account) 3370 for titer_ in titers: 3371 path = model.get_path(titer_) 3372 self.tree.expand_row(path, False) 3373 elif type_ == 'account': 3374 account = list(accounts)[0] # There is only one cause we don't use merge 3375 if account in self.collapsed_rows: 3376 self.collapsed_rows.remove(account) 3377 self.draw_account(account) 3378 # When we expand, groups are collapsed. Restore expand state 3379 for group in app.groups[account]: 3380 if app.groups[account][group]['expand']: 3381 titer = self._get_group_iter(group, account) 3382 if titer: 3383 path = model.get_path(titer) 3384 self.tree.expand_row(path, False) 3385 elif type_ == 'contact': 3386 # Metacontact got toggled, update icon 3387 jid = model[titer][Column.JID] 3388 account = model[titer][Column.ACCOUNT] 3389 contact = app.contacts.get_contact(account, jid) 3390 for group in contact.groups: 3391 if account + group + jid in self.collapsed_rows: 3392 self.collapsed_rows.remove(account + group + jid) 3393 family = app.contacts.get_metacontacts_family(account, jid) 3394 nearby_family = \ 3395 self._get_nearby_family_and_big_brother(family, account)[0] 3396 # Redraw all brothers to show pending events 3397 for data in nearby_family: 3398 self.draw_contact(data['jid'], data['account']) 3399 3400 self._toggeling_row = False 3401 3402 def on_roster_treeview_row_collapsed(self, widget, titer, path): 3403 """ 3404 When a row is collapsed change the icon of the arrow 3405 """ 3406 self._toggeling_row = True 3407 model = widget.get_model() 3408 child_model = model.get_model() 3409 child_iter = model.convert_iter_to_child_iter(titer) 3410 3411 if self.regroup: # merged accounts 3412 accounts = list(app.connections.keys()) 3413 else: 3414 accounts = [model[titer][Column.ACCOUNT]] 3415 3416 type_ = model[titer][Column.TYPE] 3417 if type_ == 'group': 3418 child_model[child_iter][Column.IMG] = get_icon_name('closed') 3419 if self.rfilter_enabled: 3420 return 3421 group = model[titer][Column.JID] 3422 for account in accounts: 3423 if group in app.groups[account]: # This account has this group 3424 app.groups[account][group]['expand'] = False 3425 if account + group not in self.collapsed_rows: 3426 self.collapsed_rows.append(account + group) 3427 elif type_ == 'account': 3428 account = accounts[0] # There is only one cause we don't use merge 3429 if account not in self.collapsed_rows: 3430 self.collapsed_rows.append(account) 3431 self.draw_account(account) 3432 elif type_ == 'contact': 3433 # Metacontact got toggled, update icon 3434 jid = model[titer][Column.JID] 3435 account = model[titer][Column.ACCOUNT] 3436 contact = app.contacts.get_contact(account, jid) 3437 groups = contact.groups 3438 if not groups: 3439 groups = [_('General')] 3440 for group in groups: 3441 if account + group + jid not in self.collapsed_rows: 3442 self.collapsed_rows.append(account + group + jid) 3443 family = app.contacts.get_metacontacts_family(account, jid) 3444 nearby_family = \ 3445 self._get_nearby_family_and_big_brother(family, account)[0] 3446 # Redraw all brothers to show pending events 3447 for data in nearby_family: 3448 self.draw_contact(data['jid'], data['account']) 3449 3450 self._toggeling_row = False 3451 3452 def on_modelfilter_row_has_child_toggled(self, model, path, titer): 3453 """ 3454 Called when a row has gotten the first or lost its last child row 3455 3456 Expand Parent if necessary. 3457 """ 3458 if self._toggeling_row: 3459 # Signal is emitted when we write to our model 3460 return 3461 3462 type_ = model[titer][Column.TYPE] 3463 account = model[titer][Column.ACCOUNT] 3464 if not account: 3465 return 3466 3467 if type_ == 'contact': 3468 child_iter = model.convert_iter_to_child_iter(titer) 3469 if self.model.iter_has_child(child_iter): 3470 # we are a bigbrother metacontact 3471 # redraw us to show/hide expand icon 3472 if self.filtering: 3473 # Prevent endless loops 3474 jid = model[titer][Column.JID] 3475 GLib.idle_add(self.draw_contact, jid, account) 3476 elif type_ == 'group': 3477 group = model[titer][Column.JID] 3478 GLib.idle_add(self._adjust_group_expand_collapse_state, group, account) 3479 elif type_ == 'account': 3480 GLib.idle_add(self._adjust_account_expand_collapse_state, account) 3481 3482# Selection can change when the model is filtered 3483# Only write to the model when filtering is finished! 3484# 3485# FIXME: When we are filtering our custom colors are somehow lost 3486# 3487# def on_treeview_selection_changed(self, selection): 3488# '''Called when selection in TreeView has changed. 3489# 3490# Redraw unselected rows to make status message readable 3491# on all possible backgrounds. 3492# ''' 3493# model, list_of_paths = selection.get_selected_rows() 3494# if len(self._last_selected_contact): 3495# # update unselected rows 3496# for (jid, account) in self._last_selected_contact: 3497# GLib.idle_add(self.draw_contact, jid, 3498# account) 3499# self._last_selected_contact = [] 3500# if len(list_of_paths) == 0: 3501# return 3502# for path in list_of_paths: 3503# row = model[path] 3504# if row[Column.TYPE] != 'contact': 3505# self._last_selected_contact = [] 3506# return 3507# jid = row[Column.JID] 3508# account = row[Column.ACCOUNT] 3509# self._last_selected_contact.append((jid, account)) 3510# GLib.idle_add(self.draw_contact, jid, account, True) 3511 3512 3513 def on_service_disco_menuitem_activate(self, widget, account): 3514 server_jid = app.settings.get_account_setting(account, 'hostname') 3515 if server_jid in app.interface.instances[account]['disco']: 3516 app.interface.instances[account]['disco'][server_jid].\ 3517 window.present() 3518 else: 3519 try: 3520 # Object will add itself to the window dict 3521 ServiceDiscoveryWindow(account, address_entry=True) 3522 except GajimGeneralException: 3523 pass 3524 3525 def on_show_offline_contacts_action(self, action, param): 3526 """ 3527 When show offline option is changed: redraw the treeview 3528 """ 3529 action.set_state(param) 3530 app.settings.set('showoffline', param.get_boolean()) 3531 self.refilter_shown_roster_items() 3532 self.window.lookup_action('show-active').set_enabled( 3533 not param.get_boolean()) 3534 3535 def on_show_active_contacts_action(self, action, param): 3536 """ 3537 When show only active contact option is changed: redraw the treeview 3538 """ 3539 action.set_state(param) 3540 app.settings.set('show_only_chat_and_online', param.get_boolean()) 3541 self.refilter_shown_roster_items() 3542 self.window.lookup_action('show-offline').set_enabled( 3543 not param.get_boolean()) 3544 3545 def on_show_roster_action(self, action, param): 3546 # when num controls is 0 this menuitem is hidden, but still need to 3547 # disable keybinding 3548 action.set_state(param) 3549 if self.hpaned.get_child2() is not None: 3550 self.show_roster_vbox(param.get_boolean()) 3551 3552 def on_rfilter_entry_changed(self, widget): 3553 """ When we update the content of the filter """ 3554 self.rfilter_string = widget.get_text().lower() 3555 if self.rfilter_string == '': 3556 self.disable_rfilter() 3557 self.refilter_shown_roster_items() 3558 # select first row 3559 self.tree.get_selection().unselect_all() 3560 def _func(model, path, iter_, param): 3561 if model[iter_][Column.TYPE] == 'contact' and self.rfilter_string in \ 3562 model[iter_][Column.NAME].lower(): 3563 col = self.tree.get_column(0) 3564 self.tree.set_cursor_on_cell(path, col, None, False) 3565 return True 3566 self.modelfilter.foreach(_func, None) 3567 3568 def on_rfilter_entry_icon_press(self, widget, icon, event): 3569 """ 3570 Disable the roster filtering by clicking the icon in the textEntry 3571 """ 3572 self.disable_rfilter() 3573 3574 def on_rfilter_entry_key_press_event(self, widget, event): 3575 if event.keyval == Gdk.KEY_Escape: 3576 self.disable_rfilter() 3577 elif event.keyval == Gdk.KEY_Return: 3578 self.tree.grab_focus() 3579 self.tree.event(event) 3580 self.disable_rfilter() 3581 elif event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down): 3582 self.tree.grab_focus() 3583 self.tree.event(event) 3584 elif event.keyval == Gdk.KEY_BackSpace: 3585 if widget.get_text() == '': 3586 self.disable_rfilter() 3587 3588 def enable_rfilter(self, search_string): 3589 self.rfilter_entry.set_visible(True) 3590 self.rfilter_entry.set_editable(True) 3591 self.rfilter_entry.grab_focus() 3592 if self.rfilter_enabled: 3593 self.rfilter_entry.set_text(self.rfilter_entry.get_text() + \ 3594 search_string) 3595 else: 3596 self.rfilter_enabled = True 3597 self.rfilter_entry.set_text(search_string) 3598 self.tree.expand_all() 3599 self.rfilter_entry.set_position(-1) 3600 3601 # If roster is hidden, let's temporarily show it. This can happen if user 3602 # enables rfilter via keyboard shortcut. 3603 self.show_roster_vbox(True) 3604 3605 def disable_rfilter(self): 3606 self.rfilter_enabled = False 3607 self.rfilter_entry.set_text('') 3608 self.rfilter_entry.set_visible(False) 3609 self.rfilter_entry.set_editable(False) 3610 self.refilter_shown_roster_items() 3611 self.tree.grab_focus() 3612 self._readjust_expand_collapse_state() 3613 3614 # If roster was hidden before enable_rfilter was called, hide it back. 3615 state = self.window.lookup_action('show-roster').get_state().get_boolean() 3616 if state is False and self.hpaned.get_child2() is not None: 3617 self.show_roster_vbox(False) 3618 3619 def on_roster_hpaned_notify(self, pane, gparamspec): 3620 """ 3621 Keep changing the width of the roster 3622 (when a Gtk.Paned widget handle is dragged) 3623 """ 3624 if gparamspec and gparamspec.name == 'position': 3625 roster_width = pane.get_child1().get_allocation().width 3626 app.settings.set('roster_width', roster_width) 3627 app.settings.set('roster_hpaned_position', pane.get_position()) 3628 3629################################################################################ 3630### Drag and Drop handling 3631################################################################################ 3632 3633 def drag_data_get_data(self, treeview, context, selection, target_id, 3634 etime): 3635 model, list_of_paths = self.tree.get_selection().get_selected_rows() 3636 if len(list_of_paths) != 1: 3637 return 3638 path = list_of_paths[0] 3639 data = '' 3640 if path.get_depth() >= 2: 3641 data = model[path][Column.JID] 3642 selection.set_text(data, -1) 3643 3644 def drag_begin(self, treeview, context): 3645 self.dragging = True 3646 3647 def drag_end(self, treeview, context): 3648 self.dragging = False 3649 3650 def on_drop_rosterx(self, widget, account_source, c_source, account_dest, 3651 c_dest, was_big_brother, context, etime): 3652 type_ = 'message' 3653 if (c_dest.show not in ('offline', 'error') and 3654 c_dest.supports(Namespace.ROSTERX)): 3655 type_ = 'iq' 3656 con = app.connections[account_dest] 3657 con.get_module('RosterItemExchange').send_contacts( 3658 [c_source], c_dest.get_full_jid(), type_=type_) 3659 3660 def on_drop_in_contact(self, widget, account_source, c_source, account_dest, 3661 c_dest, was_big_brother, context, etime): 3662 con_source = app.connections[account_source] 3663 con_dest = app.connections[account_dest] 3664 if (not con_source.get_module('MetaContacts').available or 3665 not con_dest.get_module('MetaContacts').available): 3666 return 3667 3668 def merge_contacts(is_checked=None): 3669 contacts = 0 3670 if is_checked is not None: # dialog has been shown 3671 if is_checked: # user does not want to be asked again 3672 app.settings.set('confirm_metacontacts', 'no') 3673 else: 3674 app.settings.set('confirm_metacontacts', 'yes') 3675 3676 # We might have dropped on a metacontact. 3677 # Remove it and add it again later with updated family info 3678 dest_family = app.contacts.get_metacontacts_family(account_dest, 3679 c_dest.jid) 3680 if dest_family: 3681 self._remove_metacontact_family(dest_family, account_dest) 3682 source_family = app.contacts.get_metacontacts_family( 3683 account_source, c_source.jid) 3684 if dest_family == source_family: 3685 n = contacts = len(dest_family) 3686 for tag in source_family: 3687 if tag['jid'] == c_source.jid: 3688 tag['order'] = contacts 3689 continue 3690 if 'order' in tag: 3691 n -= 1 3692 tag['order'] = n 3693 else: 3694 self._remove_entity(c_dest, account_dest) 3695 3696 old_family = app.contacts.get_metacontacts_family(account_source, 3697 c_source.jid) 3698 old_groups = c_source.groups 3699 3700 # Remove old source contact(s) 3701 if was_big_brother: 3702 # We have got little brothers. Add them all back 3703 self._remove_metacontact_family(old_family, account_source) 3704 else: 3705 # We are only a little brother. Simply remove us from our big 3706 # brother 3707 if self._get_contact_iter(c_source.jid, account_source): 3708 # When we have been in the group before. 3709 # Do not try to remove us again 3710 self._remove_entity(c_source, account_source) 3711 3712 own_data = {} 3713 own_data['jid'] = c_source.jid 3714 own_data['account'] = account_source 3715 # Don't touch the rest of the family 3716 old_family = [own_data] 3717 3718 # Apply new tag and update contact 3719 for data in old_family: 3720 if account_source != data['account'] and not self.regroup: 3721 continue 3722 3723 _account = data['account'] 3724 _jid = data['jid'] 3725 _contact = app.contacts.get_first_contact_from_jid(_account, 3726 _jid) 3727 if not _contact: 3728 # One of the metacontacts may be not connected. 3729 continue 3730 3731 _contact.groups = c_dest.groups[:] 3732 app.contacts.add_metacontact(account_dest, c_dest.jid, 3733 _account, _contact.jid, contacts) 3734 con = app.connections[account_source] 3735 con.get_module('Roster').update_contact( 3736 _contact.jid, _contact.name, _contact.groups) 3737 3738 # Re-add all and update GUI 3739 new_family = app.contacts.get_metacontacts_family(account_source, 3740 c_source.jid) 3741 brothers = self._add_metacontact_family(new_family, account_source) 3742 3743 for c, acc in brothers: 3744 self.draw_completely(c.jid, acc) 3745 3746 old_groups.extend(c_dest.groups) 3747 for g in old_groups: 3748 self.draw_group(g, account_source) 3749 3750 self.draw_account(account_source) 3751 context.finish(True, True, etime) 3752 3753 dest_family = app.contacts.get_metacontacts_family(account_dest, 3754 c_dest.jid) 3755 source_family = app.contacts.get_metacontacts_family(account_source, 3756 c_source.jid) 3757 confirm_metacontacts = app.settings.get('confirm_metacontacts') 3758 if confirm_metacontacts == 'no' or dest_family == source_family: 3759 merge_contacts() 3760 return 3761 pritext = _('You are about to create a metacontact') 3762 sectext = _('Metacontacts are a way to regroup several contacts in ' 3763 'one single contact. Generally it is used when the same ' 3764 'person has several XMPP- or Transport-Accounts.') 3765 ConfirmationCheckDialog( 3766 _('Create Metacontact'), 3767 pritext, 3768 sectext, 3769 _('_Do not ask me again'), 3770 [DialogButton.make('Cancel'), 3771 DialogButton.make('Accept', 3772 text=_('_Create'), 3773 callback=merge_contacts)]).show() 3774 3775 def on_drop_in_group(self, widget, account, c_source, grp_dest, 3776 is_big_brother, context, etime, grp_source=None): 3777 if is_big_brother: 3778 # add whole metacontact to new group 3779 self.add_contact_to_groups(c_source.jid, account, [grp_dest, ]) 3780 # remove afterwards so the contact is not moved to General in the 3781 # meantime 3782 if grp_dest != grp_source: 3783 self.remove_contact_from_groups(c_source.jid, account, 3784 [grp_source]) 3785 else: 3786 # Normal contact or little brother 3787 family = app.contacts.get_metacontacts_family(account, 3788 c_source.jid) 3789 if family: 3790 # Little brother 3791 # Remove whole family. Remove us from the family. 3792 # Then re-add other family members. 3793 self._remove_metacontact_family(family, account) 3794 app.contacts.remove_metacontact(account, c_source.jid) 3795 for data in family: 3796 if account != data['account'] and not self.regroup: 3797 continue 3798 if data['jid'] == c_source.jid and\ 3799 data['account'] == account: 3800 continue 3801 self.add_contact(data['jid'], data['account']) 3802 break 3803 3804 self.add_contact_to_groups(c_source.jid, account, [grp_dest, ]) 3805 3806 else: 3807 # Normal contact 3808 self.add_contact_to_groups(c_source.jid, account, [grp_dest, ]) 3809 # remove afterwards so the contact is not moved to General in 3810 # the meantime 3811 if grp_dest != grp_source: 3812 self.remove_contact_from_groups(c_source.jid, account, 3813 [grp_source]) 3814 3815 if context.get_actions() in (Gdk.DragAction.MOVE, Gdk.DragAction.COPY): 3816 context.finish(True, True, etime) 3817 3818 def drag_drop(self, treeview, context, x, y, timestamp): 3819 treeview.stop_emission_by_name('drag-drop') 3820 target_list = treeview.drag_dest_get_target_list() 3821 target = treeview.drag_dest_find_target(context, target_list) 3822 treeview.drag_get_data(context, target, timestamp) 3823 return True 3824 3825 def move_group(self, old_name, new_name, account): 3826 for group in list(app.groups[account].keys()): 3827 if group.startswith(old_name): 3828 self.rename_group(group, group.replace(old_name, new_name), 3829 account) 3830 3831 def drag_data_received_data(self, treeview, context, x, y, selection, info, 3832 etime): 3833 treeview.stop_emission_by_name('drag-data-received') 3834 drop_info = treeview.get_dest_row_at_pos(x, y) 3835 if not drop_info: 3836 return 3837 data = selection.get_data().decode() 3838 if not data: 3839 return # prevents tb when several entries are dragged 3840 model = treeview.get_model() 3841 3842 path_dest, position = drop_info 3843 3844 if position == Gtk.TreeViewDropPosition.BEFORE and len(path_dest) == 2 \ 3845 and path_dest[1] == 0: # dropped before the first group 3846 return 3847 if position == Gtk.TreeViewDropPosition.BEFORE and len(path_dest) == 2: 3848 # dropped before a group: we drop it in the previous group every 3849 # time 3850 path_dest = (path_dest[0], path_dest[1]-1) 3851 # destination: the row something got dropped on 3852 iter_dest = model.get_iter(path_dest) 3853 type_dest = model[iter_dest][Column.TYPE] 3854 jid_dest = model[iter_dest][Column.JID] 3855 account_dest = model[iter_dest][Column.ACCOUNT] 3856 3857 # drop on account row in merged mode, we cannot know the desired account 3858 if account_dest == 'all': 3859 return 3860 # nothing can be done, if destination account is offline 3861 if not app.account_is_available(account_dest): 3862 return 3863 3864 # A file got dropped on the roster 3865 if info == self.TARGET_TYPE_URI_LIST: 3866 if len(path_dest) < 3: 3867 return 3868 if type_dest != 'contact': 3869 return 3870 c_dest = app.contacts.get_contact_with_highest_priority( 3871 account_dest, jid_dest) 3872 if not c_dest.supports(Namespace.JINGLE_FILE_TRANSFER_5): 3873 return 3874 uri = data.strip() 3875 uri_splitted = uri.split() # we may have more than one file dropped 3876 try: 3877 # This is always the last element in windows 3878 uri_splitted.remove('\0') 3879 except ValueError: 3880 pass 3881 nb_uri = len(uri_splitted) 3882 # Check the URIs 3883 bad_uris = [] 3884 for a_uri in uri_splitted: 3885 path = helpers.get_file_path_from_dnd_dropped_uri(a_uri) 3886 if not os.path.isfile(path): 3887 bad_uris.append(a_uri) 3888 if bad_uris: 3889 ErrorDialog(_('Invalid file URI:'), '\n'.join(bad_uris)) 3890 return 3891 def _on_send_files(account, jid, uris): 3892 c = app.contacts.get_contact_with_highest_priority(account, 3893 jid) 3894 for uri in uris: 3895 path = helpers.get_file_path_from_dnd_dropped_uri(uri) 3896 if os.path.isfile(path): # is it file? 3897 app.interface.instances['file_transfers'].send_file( 3898 account, c, path) 3899 # Popup dialog to confirm sending 3900 text = i18n.ngettext( 3901 'Send this file to %s:\n', 3902 'Send these files to %s:\n', 3903 nb_uri) % c_dest.get_shown_name() 3904 3905 for uri in uri_splitted: 3906 path = helpers.get_file_path_from_dnd_dropped_uri(uri) 3907 text += '\n' + os.path.basename(path) 3908 ConfirmationDialog( 3909 _('File Transfer'), 3910 _('File Transfer'), 3911 text, 3912 [DialogButton.make('Cancel'), 3913 DialogButton.make('Accept', 3914 text=_('_Send'), 3915 callback=_on_send_files, 3916 args=(account_dest, jid_dest, uri_splitted))], 3917 transient_for=self.window).show() 3918 return 3919 3920 # Check if something is selected 3921 if treeview.get_selection().count_selected_rows() == 0: 3922 return 3923 3924 # a roster entry was dragged and dropped somewhere in the roster 3925 3926 # source: the row that was dragged 3927 path_source = treeview.get_selection().get_selected_rows()[1][0] 3928 iter_source = model.get_iter(path_source) 3929 type_source = model[iter_source][Column.TYPE] 3930 account_source = model[iter_source][Column.ACCOUNT] 3931 3932 if app.settings.get_account_setting(account_source, 'is_zeroconf'): 3933 return 3934 3935 if type_dest == 'self_contact': 3936 # drop on self contact row 3937 return 3938 3939 if type_dest == 'groupchat': 3940 # Drop on a minimized groupchat 3941 if type_source != 'contact': 3942 return 3943 contact_jid = data 3944 gc_control = app.get_groupchat_control(account_dest, jid_dest) 3945 if gc_control is not None: 3946 gc_control.invite(contact_jid) 3947 return 3948 3949 if type_source == 'group': 3950 if account_source != account_dest: 3951 # drop on another account 3952 return 3953 grp_source = model[iter_source][Column.JID] 3954 delimiter = app.connections[account_source].get_module('Delimiter').delimiter 3955 grp_source_list = grp_source.split(delimiter) 3956 new_grp = None 3957 if type_dest == 'account': 3958 new_grp = grp_source_list[-1] 3959 elif type_dest == 'group': 3960 grp_dest = model[iter_dest][Column.JID] 3961 # Don't allow to drop on e.g. Groupchats group 3962 if grp_dest in helpers.special_groups: 3963 return 3964 grp_dest_list = grp_dest.split(delimiter) 3965 # Do not allow to drop on a subgroup of source group 3966 if grp_source_list[0] != grp_dest_list[0]: 3967 new_grp = model[iter_dest][Column.JID] + delimiter + \ 3968 grp_source_list[-1] 3969 if new_grp: 3970 self.move_group(grp_source, new_grp, account_source) 3971 3972 # Only normal contacts and group can be dragged 3973 if type_source != 'contact': 3974 return 3975 3976 # A contact was dropped 3977 if app.settings.get_account_setting(account_dest, 'is_zeroconf'): 3978 # drop on zeroconf account, adding not possible 3979 return 3980 3981 if type_dest == 'account' and account_source == account_dest: 3982 # drop on the account it was dragged from 3983 return 3984 3985 # Get valid source group, jid and contact 3986 it = iter_source 3987 while model[it][Column.TYPE] == 'contact': 3988 it = model.iter_parent(it) 3989 grp_source = model[it][Column.JID] 3990 if grp_source in (_('Transports'), _('Group chats')): 3991 # a transport or a minimized groupchat was dragged 3992 # we can add it to other accounts but not move it to another group, 3993 # see below 3994 return 3995 jid_source = data 3996 c_source = app.contacts.get_contact_with_highest_priority( 3997 account_source, jid_source) 3998 3999 # Get destination group 4000 grp_dest = None 4001 if type_dest == 'group': 4002 grp_dest = model[iter_dest][Column.JID] 4003 elif type_dest in ('contact', 'agent'): 4004 it = iter_dest 4005 while model[it][Column.TYPE] != 'group': 4006 it = model.iter_parent(it) 4007 grp_dest = model[it][Column.JID] 4008 if grp_dest in helpers.special_groups: 4009 return 4010 4011 if jid_source == jid_dest: 4012 if grp_source == grp_dest and account_source == account_dest: 4013 # Drop on self 4014 return 4015 4016 # contact drop somewhere in or on a foreign account 4017 if (type_dest == 'account' or not self.regroup) and \ 4018 account_source != account_dest: 4019 # add to account in specified group 4020 AddNewContactWindow(account=account_dest, contact_jid=jid_source, 4021 user_nick=c_source.name, group=grp_dest) 4022 return 4023 4024 # we may not add contacts from special_groups 4025 if grp_source in helpers.special_groups: 4026 if grp_source == _('Not in contact list'): 4027 AddNewContactWindow( 4028 account=account_dest, 4029 contact_jid=jid_source, 4030 user_nick=c_source.name, 4031 group=grp_dest) 4032 return 4033 return 4034 4035 # Is the contact we drag a meta contact? 4036 accounts = account_source 4037 if self.regroup: 4038 accounts = app.contacts.get_accounts() or account_source 4039 is_big_brother = app.contacts.is_big_brother(account_source, 4040 jid_source, accounts) 4041 4042 drop_in_middle_of_meta = False 4043 if type_dest == 'contact': 4044 if position == Gtk.TreeViewDropPosition.BEFORE and len(path_dest) == 4: 4045 drop_in_middle_of_meta = True 4046 if position == Gtk.TreeViewDropPosition.AFTER and (len(path_dest) == 4 or\ 4047 self.modelfilter.iter_has_child(iter_dest)): 4048 drop_in_middle_of_meta = True 4049 # Contact drop on group row or between two contacts that are 4050 # not metacontacts 4051 if (type_dest == 'group' or position in (Gtk.TreeViewDropPosition.BEFORE, 4052 Gtk.TreeViewDropPosition.AFTER)) and not drop_in_middle_of_meta: 4053 self.on_drop_in_group(None, account_source, c_source, grp_dest, 4054 is_big_brother, context, etime, grp_source) 4055 return 4056 4057 # Contact drop on another contact, make meta contacts 4058 if position == Gtk.TreeViewDropPosition.INTO_OR_AFTER or \ 4059 position == Gtk.TreeViewDropPosition.INTO_OR_BEFORE or drop_in_middle_of_meta: 4060 c_dest = app.contacts.get_contact_with_highest_priority( 4061 account_dest, jid_dest) 4062 if not c_dest: 4063 # c_dest is None if jid_dest doesn't belong to account 4064 return 4065 menu = Gtk.Menu() 4066 #from and to are the names of contacts 4067 item = Gtk.MenuItem.new_with_label(_('Send %(from)s to %(to)s') % { 4068 'from': c_source.get_shown_name(), 'to': c_dest.get_shown_name()}) 4069 item.set_use_underline(False) 4070 item.connect('activate', self.on_drop_rosterx, account_source, 4071 c_source, account_dest, c_dest, is_big_brother, context, etime) 4072 menu.append(item) 4073 4074 dest_family = app.contacts.get_metacontacts_family(account_dest, 4075 c_dest.jid) 4076 source_family = app.contacts.get_metacontacts_family( 4077 account_source, c_source.jid) 4078 if dest_family == source_family and dest_family: 4079 item = Gtk.MenuItem.new_with_label( 4080 _('Make %s first contact') % ( 4081 c_source.get_shown_name())) 4082 item.set_use_underline(False) 4083 else: 4084 item = Gtk.MenuItem.new_with_label( 4085 _('Make %(contact1)s and %(contact2)s metacontacts') % { 4086 'contact1': c_source.get_shown_name(), 'contact2': c_dest.get_shown_name()}) 4087 item.set_use_underline(False) 4088 4089 item.connect('activate', self.on_drop_in_contact, account_source, 4090 c_source, account_dest, c_dest, is_big_brother, context, etime) 4091 4092 menu.append(item) 4093 4094 menu.attach_to_widget(self.tree, None) 4095 menu.connect('selection-done', gtkgui_helpers.destroy_widget) 4096 menu.show_all() 4097 menu.popup_at_pointer(None) 4098 4099################################################################################ 4100### Everything about images and icons.... 4101### Cleanup assigned to Jim++ :-) 4102################################################################################ 4103 4104 def update_icons(self): 4105 # Update the roster 4106 self.setup_and_draw_roster() 4107 4108 # Update the systray 4109 if app.interface.systray_enabled: 4110 app.interface.systray.set_img() 4111 app.interface.systray.change_status(helpers.get_global_show()) 4112 4113 for win in app.interface.msg_win_mgr.windows(): 4114 for ctrl in win.controls(): 4115 ctrl.update_ui() 4116 win.redraw_tab(ctrl) 4117 4118 self._status_selector.update() 4119 4120 4121 def set_account_status_icon(self, account): 4122 child_iterA = self._get_account_iter(account, self.model) 4123 if not child_iterA: 4124 return 4125 if not self.regroup: 4126 status = helpers.get_connection_status(account) 4127 else: # accounts merged 4128 status = helpers.get_global_show() 4129 self.model[child_iterA][Column.IMG] = get_icon_name(status) 4130 4131################################################################################ 4132### Style and theme related methods 4133################################################################################ 4134 4135 def show_title(self): 4136 change_title_allowed = app.settings.get('change_roster_title') 4137 if not change_title_allowed: 4138 return 4139 4140 nb_unread = 0 4141 for account in app.connections: 4142 # Count events in roster title only if we don't auto open them 4143 if not helpers.allow_popup_window(account): 4144 nb_unread += app.events.get_nb_events(['chat', 'normal', 4145 'file-request', 'file-error', 'file-completed', 4146 'file-request-error', 'file-send-error', 'file-stopped', 4147 'printed_chat'], account) 4148 4149 4150 if app.settings.get('one_message_window') == 'always_with_roster': 4151 # always_with_roster mode defers to the MessageWindow 4152 if not app.interface.msg_win_mgr.one_window_opened(): 4153 # No MessageWindow to defer to 4154 self.window.set_title('Gajim') 4155 set_urgency_hint(self.window, nb_unread > 0) 4156 return 4157 4158 start = '' 4159 if nb_unread > 1: 4160 start = '[' + str(nb_unread) + '] ' 4161 elif nb_unread == 1: 4162 start = '* ' 4163 4164 self.window.set_title(start + 'Gajim') 4165 set_urgency_hint(self.window, nb_unread > 0) 4166 4167 def _nec_chatstate_received(self, event): 4168 if event.contact.is_gc_contact or event.contact.is_pm_contact: 4169 return 4170 self.draw_contact(event.contact.jid, event.account) 4171 4172 def _style_changed(self, *args): 4173 self.change_roster_style(None) 4174 4175 def _change_style(self, model, path, titer, option): 4176 if option is None or model[titer][Column.TYPE] == option: 4177 # We changed style for this type of row 4178 model[titer][Column.NAME] = model[titer][Column.NAME] 4179 4180 def change_roster_style(self, option): 4181 self.model.foreach(self._change_style, option) 4182 for win in app.interface.msg_win_mgr.windows(): 4183 win.repaint_themed_widgets() 4184 4185 def repaint_themed_widgets(self): 4186 """ 4187 Notify windows that contain themed widgets to repaint them 4188 """ 4189 for win in app.interface.msg_win_mgr.windows(): 4190 win.repaint_themed_widgets() 4191 for account in app.connections: 4192 for ctrl in list(app.interface.minimized_controls[account].values()): 4193 ctrl.repaint_themed_widgets() 4194 4195 def _iconCellDataFunc(self, column, renderer, model, titer, data=None): 4196 """ 4197 When a row is added, set properties for icon renderer 4198 """ 4199 icon_name = model[titer][Column.IMG] 4200 if ':' in icon_name: 4201 icon_name, expanded = icon_name.split(':') 4202 surface = get_metacontact_surface( 4203 icon_name, expanded == 'opened', self.scale_factor) 4204 renderer.set_property('icon_name', None) 4205 renderer.set_property('surface', surface) 4206 else: 4207 renderer.set_property('surface', None) 4208 renderer.set_property('icon_name', icon_name) 4209 4210 try: 4211 type_ = model[titer][Column.TYPE] 4212 except TypeError: 4213 return 4214 if type_ == 'account': 4215 self._set_account_row_background_color(renderer) 4216 renderer.set_property('xalign', 0) 4217 elif type_ == 'group': 4218 self._set_group_row_background_color(renderer) 4219 parent_iter = model.iter_parent(titer) 4220 if model[parent_iter][Column.TYPE] == 'group': 4221 renderer.set_property('xalign', 0.4) 4222 else: 4223 renderer.set_property('xalign', 0.6) 4224 elif type_: 4225 # prevent type_ = None, see http://trac.gajim.org/ticket/2534 4226 if not model[titer][Column.JID] or not model[titer][Column.ACCOUNT]: 4227 # This can append when at the moment we add the row 4228 return 4229 jid = model[titer][Column.JID] 4230 account = model[titer][Column.ACCOUNT] 4231 self._set_contact_row_background_color(renderer, jid, account) 4232 parent_iter = model.iter_parent(titer) 4233 if model[parent_iter][Column.TYPE] == 'contact': 4234 renderer.set_property('xalign', 1) 4235 else: 4236 renderer.set_property('xalign', 0.6) 4237 renderer.set_property('width', 26) 4238 4239 def _nameCellDataFunc(self, column, renderer, model, titer, data=None): 4240 """ 4241 When a row is added, set properties for name renderer 4242 """ 4243 try: 4244 type_ = model[titer][Column.TYPE] 4245 except TypeError: 4246 return 4247 4248 if type_ == 'account': 4249 color = app.css_config.get_value('.gajim-account-row', StyleAttr.COLOR) 4250 renderer.set_property('foreground', color) 4251 desc = app.css_config.get_font('.gajim-account-row') 4252 renderer.set_property('font-desc', desc) 4253 renderer.set_property('xpad', 0) 4254 renderer.set_property('width', 3) 4255 self._set_account_row_background_color(renderer) 4256 elif type_ == 'group': 4257 color = app.css_config.get_value('.gajim-group-row', StyleAttr.COLOR) 4258 renderer.set_property('foreground', color) 4259 desc = app.css_config.get_font('.gajim-group-row') 4260 renderer.set_property('font-desc', desc) 4261 parent_iter = model.iter_parent(titer) 4262 if model[parent_iter][Column.TYPE] == 'group': 4263 renderer.set_property('xpad', 8) 4264 else: 4265 renderer.set_property('xpad', 4) 4266 self._set_group_row_background_color(renderer) 4267 elif type_: 4268 # prevent type_ = None, see http://trac.gajim.org/ticket/2534 4269 if not model[titer][Column.JID] or not model[titer][Column.ACCOUNT]: 4270 # This can append when at the moment we add the row 4271 return 4272 jid = model[titer][Column.JID] 4273 account = model[titer][Column.ACCOUNT] 4274 4275 color = None 4276 if type_ == 'groupchat': 4277 ctrl = app.interface.minimized_controls[account].get(jid, None) 4278 if ctrl and ctrl.attention_flag: 4279 color = app.css_config.get_value( 4280 '.state_muc_directed_msg_color', StyleAttr.COLOR) 4281 elif app.settings.get('show_chatstate_in_roster'): 4282 chatstate = app.contacts.get_combined_chatstate(account, jid) 4283 if chatstate not in (None, 'active'): 4284 color = app.css_config.get_value( 4285 '.gajim-state-%s' % chatstate, StyleAttr.COLOR) 4286 else: 4287 color = app.css_config.get_value( 4288 '.gajim-contact-row', StyleAttr.COLOR) 4289 renderer.set_property('foreground', color) 4290 4291 self._set_contact_row_background_color(renderer, jid, account) 4292 desc = app.css_config.get_font('.gajim-contact-row') 4293 renderer.set_property('font-desc', desc) 4294 parent_iter = model.iter_parent(titer) 4295 if model[parent_iter][Column.TYPE] == 'contact': 4296 renderer.set_property('xpad', 16) 4297 else: 4298 renderer.set_property('xpad', 12) 4299 4300 def _fill_pep_pixbuf_renderer(self, column, renderer, model, titer, 4301 data=None): 4302 """ 4303 When a row is added, draw the respective pep icon 4304 """ 4305 try: 4306 type_ = model[titer][Column.TYPE] 4307 except TypeError: 4308 return 4309 4310 # allocate space for the icon only if needed 4311 if model[titer][data] is None: 4312 renderer.set_property('visible', False) 4313 else: 4314 renderer.set_property('visible', True) 4315 4316 if type_ == 'account': 4317 self._set_account_row_background_color(renderer) 4318 renderer.set_property('xalign', 1) 4319 elif type_: 4320 if not model[titer][Column.JID] or not model[titer][Column.ACCOUNT]: 4321 # This can append at the moment we add the row 4322 return 4323 jid = model[titer][Column.JID] 4324 account = model[titer][Column.ACCOUNT] 4325 self._set_contact_row_background_color(renderer, jid, account) 4326 4327 def _fill_avatar_pixbuf_renderer(self, column, renderer, model, titer, 4328 data=None): 4329 """ 4330 When a row is added, set properties for avatar renderer 4331 """ 4332 try: 4333 type_ = model[titer][Column.TYPE] 4334 except TypeError: 4335 return 4336 4337 if type_ in ('group', 'account'): 4338 renderer.set_property('visible', False) 4339 return 4340 4341 image = model[titer][Column.AVATAR_IMG] 4342 if image is not None: 4343 surface = image.get_property('surface') 4344 renderer.set_property('surface', surface) 4345 # allocate space for the icon only if needed 4346 if model[titer][Column.AVATAR_IMG] or \ 4347 app.settings.get('avatar_position_in_roster') == 'left': 4348 renderer.set_property('visible', True) 4349 if type_: 4350 # prevent type_ = None, see http://trac.gajim.org/ticket/2534 4351 if not model[titer][Column.JID] or not model[titer][Column.ACCOUNT]: 4352 # This can append at the moment we add the row 4353 return 4354 jid = model[titer][Column.JID] 4355 account = model[titer][Column.ACCOUNT] 4356 self._set_contact_row_background_color(renderer, jid, account) 4357 else: 4358 renderer.set_property('visible', False) 4359 if model[titer][Column.AVATAR_IMG] is None and \ 4360 app.settings.get('avatar_position_in_roster') != 'left': 4361 renderer.set_property('visible', False) 4362 4363 renderer.set_property('width', AvatarSize.ROSTER) 4364 renderer.set_property('xalign', 0.5) 4365 4366 def _fill_padlock_pixbuf_renderer(self, column, renderer, model, titer, 4367 data=None): 4368 """ 4369 When a row is added, set properties for padlock renderer 4370 """ 4371 try: 4372 type_ = model[titer][Column.TYPE] 4373 except TypeError: 4374 return 4375 4376 # allocate space for the icon only if needed 4377 if type_ == 'account' and model[titer][Column.PADLOCK_PIXBUF]: 4378 renderer.set_property('visible', True) 4379 self._set_account_row_background_color(renderer) 4380 renderer.set_property('xalign', 1) # align pixbuf to the right 4381 else: 4382 renderer.set_property('visible', False) 4383 4384 def _set_account_row_background_color(self, renderer): 4385 color = app.css_config.get_value('.gajim-account-row', StyleAttr.BACKGROUND) 4386 renderer.set_property('cell-background', color) 4387 4388 def _set_contact_row_background_color(self, renderer, jid, account): 4389 if jid in app.newly_added[account]: 4390 renderer.set_property('cell-background', app.css_config.get_value( 4391 '.gajim-roster-connected', StyleAttr.BACKGROUND)) 4392 elif jid in app.to_be_removed[account]: 4393 renderer.set_property('cell-background', app.css_config.get_value( 4394 '.gajim-roster-disconnected', StyleAttr.BACKGROUND)) 4395 else: 4396 color = app.css_config.get_value('.gajim-contact-row', StyleAttr.BACKGROUND) 4397 renderer.set_property('cell-background', color) 4398 4399 def _set_group_row_background_color(self, renderer): 4400 color = app.css_config.get_value('.gajim-group-row', 'background') 4401 renderer.set_property('cell-background', color) 4402 4403################################################################################ 4404### Everything about building menus 4405### FIXME: We really need to make it simpler! 1465 lines are a few to much.... 4406################################################################################ 4407 4408 def build_account_menu(self, account): 4409 # we have to create our own set of icons for the menu 4410 # using self.jabber_status_images is poopoo 4411 if not app.settings.get_account_setting(account, 'is_zeroconf'): 4412 xml = get_builder('account_context_menu.ui') 4413 account_context_menu = xml.get_object('account_context_menu') 4414 4415 status_menuitem = xml.get_object('status_menuitem') 4416 add_contact_menuitem = xml.get_object('add_contact_menuitem') 4417 service_discovery_menuitem = xml.get_object( 4418 'service_discovery_menuitem') 4419 execute_command_menuitem = xml.get_object( 4420 'execute_command_menuitem') 4421 view_server_info_menuitem = xml.get_object( 4422 'view_server_info_menuitem') 4423 edit_account_menuitem = xml.get_object('edit_account_menuitem') 4424 sub_menu = Gtk.Menu() 4425 status_menuitem.set_submenu(sub_menu) 4426 4427 for show in ('online', 'away', 'xa', 'dnd'): 4428 uf_show = helpers.get_uf_show(show, use_mnemonic=True) 4429 item = Gtk.MenuItem.new_with_mnemonic(uf_show) 4430 sub_menu.append(item) 4431 item.connect('activate', self.change_status, account, show) 4432 4433 item = Gtk.SeparatorMenuItem.new() 4434 sub_menu.append(item) 4435 4436 item = Gtk.MenuItem.new_with_mnemonic(_('_Change Status Message')) 4437 sub_menu.append(item) 4438 item.connect('activate', self.on_change_status_message_activate, 4439 account) 4440 if not app.account_is_available(account): 4441 item.set_sensitive(False) 4442 4443 item = Gtk.SeparatorMenuItem.new() 4444 sub_menu.append(item) 4445 4446 uf_show = helpers.get_uf_show('offline', use_mnemonic=True) 4447 item = Gtk.MenuItem.new_with_mnemonic(uf_show) 4448 sub_menu.append(item) 4449 item.connect('activate', self.change_status, account, 'offline') 4450 4451 pep_menuitem = xml.get_object('pep_menuitem') 4452 if app.connections[account].get_module('PEP').supported: 4453 pep_submenu = Gtk.Menu() 4454 pep_menuitem.set_submenu(pep_submenu) 4455 4456 item = Gtk.CheckMenuItem(label=_('Publish Tune')) 4457 pep_submenu.append(item) 4458 if sys.platform in ('win32', 'darwin'): 4459 item.set_sensitive(False) 4460 else: 4461 active = app.settings.get_account_setting(account, 4462 'publish_tune') 4463 item.set_active(active) 4464 item.connect('toggled', self.on_publish_tune_toggled, 4465 account) 4466 4467 item = Gtk.CheckMenuItem(label=_('Publish Location')) 4468 pep_submenu.append(item) 4469 if not app.is_installed('GEOCLUE'): 4470 item.set_sensitive(False) 4471 else: 4472 active = app.settings.get_account_setting( 4473 account, 'publish_location') 4474 item.set_active(active) 4475 item.connect('toggled', self.on_publish_location_toggled, 4476 account) 4477 4478 else: 4479 pep_menuitem.set_sensitive(False) 4480 4481 edit_account_menuitem.set_detailed_action_name( 4482 'app.accounts::%s' % account) 4483 if app.connections[account].roster_supported: 4484 add_contact_menuitem.connect('activate', 4485 self.on_add_new_contact, account) 4486 else: 4487 add_contact_menuitem.set_sensitive(False) 4488 service_discovery_menuitem.connect('activate', 4489 self.on_service_disco_menuitem_activate, account) 4490 hostname = app.settings.get_account_setting(account, 'hostname') 4491 contact = app.contacts.create_contact(jid=hostname, 4492 account=account) # Fake contact 4493 execute_command_menuitem.connect('activate', 4494 self.on_execute_command, contact, account) 4495 view_server_info_menuitem.connect('activate', 4496 self.on_view_server_info, account) 4497 4498 # make some items insensitive if account is offline 4499 if not app.account_is_available(account): 4500 for widget in (add_contact_menuitem, service_discovery_menuitem, 4501 execute_command_menuitem, view_server_info_menuitem, 4502 pep_menuitem): 4503 widget.set_sensitive(False) 4504 else: 4505 xml = get_builder('zeroconf_context_menu.ui') 4506 account_context_menu = xml.get_object('zeroconf_context_menu') 4507 4508 status_menuitem = xml.get_object('status_menuitem') 4509 zeroconf_properties_menuitem = xml.get_object( 4510 'zeroconf_properties_menuitem') 4511 sub_menu = Gtk.Menu() 4512 status_menuitem.set_submenu(sub_menu) 4513 4514 for show in ('online', 'away', 'dnd'): 4515 uf_show = helpers.get_uf_show(show, use_mnemonic=True) 4516 item = Gtk.MenuItem.new_with_mnemonic(uf_show) 4517 sub_menu.append(item) 4518 item.connect('activate', self.change_status, account, show) 4519 4520 item = Gtk.SeparatorMenuItem.new() 4521 sub_menu.append(item) 4522 4523 item = Gtk.MenuItem.new_with_mnemonic(_('_Change Status Message')) 4524 sub_menu.append(item) 4525 item.connect('activate', self.on_change_status_message_activate, 4526 account) 4527 if not app.account_is_available(account): 4528 item.set_sensitive(False) 4529 4530 uf_show = helpers.get_uf_show('offline', use_mnemonic=True) 4531 item = Gtk.MenuItem.new_with_mnemonic(uf_show) 4532 sub_menu.append(item) 4533 item.connect('activate', self.change_status, account, 'offline') 4534 4535 zeroconf_properties_menuitem.set_detailed_action_name( 4536 'app.accounts::%s' % account) 4537 4538 return account_context_menu 4539 4540 def make_account_menu(self, event, titer): 4541 """ 4542 Make account's popup menu 4543 """ 4544 model = self.modelfilter 4545 account = model[titer][Column.ACCOUNT] 4546 4547 if account != 'all': # not in merged mode 4548 menu = self.build_account_menu(account) 4549 else: 4550 menu = Gtk.Menu() 4551 accounts = [] # Put accounts in a list to sort them 4552 for account in app.connections: 4553 accounts.append(account) 4554 accounts.sort() 4555 for account in accounts: 4556 label = app.get_account_label(account) 4557 item = Gtk.MenuItem.new_with_label(label) 4558 account_menu = self.build_account_menu(account) 4559 item.set_submenu(account_menu) 4560 menu.append(item) 4561 4562 event_button = gtkgui_helpers.get_possible_button_event(event) 4563 4564 menu.attach_to_widget(self.tree, None) 4565 menu.connect('selection-done', gtkgui_helpers.destroy_widget) 4566 menu.show_all() 4567 menu.popup(None, None, None, None, event_button, event.time) 4568 4569 def make_group_menu(self, event, iters): 4570 """ 4571 Make group's popup menu 4572 """ 4573 model = self.modelfilter 4574 groups = [] 4575 accounts = [] 4576 4577 list_ = [] # list of (contact, account) tuples 4578 list_online = [] # list of (contact, account) tuples 4579 4580 for titer in iters: 4581 groups.append(model[titer][Column.JID]) 4582 accounts.append(model[titer][Column.ACCOUNT]) 4583 # Don't show menu if groups of more than one account are selected 4584 if accounts[0] != model[titer][Column.ACCOUNT]: 4585 return 4586 account = accounts[0] 4587 4588 show_bookmarked = True 4589 for jid in app.contacts.get_jid_list(account): 4590 contact = app.contacts.get_contact_with_highest_priority(account, 4591 jid) 4592 for group in groups: 4593 if group in contact.get_shown_groups(): 4594 if contact.show not in ('offline', 'error'): 4595 list_online.append((contact, account)) 4596 # Check that all contacts support direct NUC invite 4597 if not contact.supports(Namespace.CONFERENCE): 4598 show_bookmarked = False 4599 list_.append((contact, account)) 4600 menu = Gtk.Menu() 4601 4602 # Make special context menu if group is Groupchats 4603 if _('Group chats') in groups: 4604 if len(groups) == 1: 4605 maximize_menuitem = Gtk.MenuItem.new_with_mnemonic( 4606 _('_Maximize All')) 4607 maximize_menuitem.connect('activate', 4608 self.on_all_groupchat_maximized, list_) 4609 menu.append(maximize_menuitem) 4610 else: 4611 return 4612 else: 4613 # Send Group Message 4614 send_group_message_item = Gtk.MenuItem.new_with_mnemonic( 4615 _('Send Group M_essage')) 4616 4617 send_group_message_submenu = Gtk.Menu() 4618 send_group_message_item.set_submenu(send_group_message_submenu) 4619 menu.append(send_group_message_item) 4620 4621 group_message_to_all_item = Gtk.MenuItem.new_with_label(_( 4622 'To all users')) 4623 send_group_message_submenu.append(group_message_to_all_item) 4624 4625 group_message_to_all_online_item = Gtk.MenuItem.new_with_label( 4626 _('To all online users')) 4627 send_group_message_submenu.append(group_message_to_all_online_item) 4628 4629 group_message_to_all_online_item.connect('activate', 4630 self.on_send_single_message_menuitem_activate, account, 4631 list_online) 4632 group_message_to_all_item.connect('activate', 4633 self.on_send_single_message_menuitem_activate, account, list_) 4634 4635 # Invite to 4636 invite_menuitem = Gtk.MenuItem.new_with_mnemonic( 4637 _('In_vite to')) 4638 if _('Transports') not in groups: 4639 gui_menu_builder.build_invite_submenu(invite_menuitem, 4640 list_online, show_bookmarked=show_bookmarked) 4641 menu.append(invite_menuitem) 4642 4643 # there is no singlemessage and custom status for zeroconf 4644 if app.settings.get_account_setting(account, 'is_zeroconf'): 4645 send_group_message_item.set_sensitive(False) 4646 4647 if not app.account_is_available(account): 4648 send_group_message_item.set_sensitive(False) 4649 invite_menuitem.set_sensitive(False) 4650 4651 special_group = False 4652 for group in groups: 4653 if group in helpers.special_groups: 4654 special_group = True 4655 break 4656 4657 if not special_group and len(groups) == 1: 4658 group = groups[0] 4659 item = Gtk.SeparatorMenuItem.new() # separator 4660 menu.append(item) 4661 4662 # Rename 4663 rename_item = Gtk.MenuItem.new_with_mnemonic(_('_Rename…')) 4664 menu.append(rename_item) 4665 rename_item.connect('activate', self.on_rename, 'group', group, 4666 account) 4667 4668 # Remove group 4669 remove_item = Gtk.MenuItem.new_with_mnemonic(_('Remo_ve')) 4670 menu.append(remove_item) 4671 remove_item.connect('activate', self.on_remove_group_item_activated, 4672 group, account) 4673 4674 # unsensitive if account is not connected 4675 if not app.account_is_available(account): 4676 rename_item.set_sensitive(False) 4677 4678 # General group cannot be changed 4679 if group == _('General'): 4680 rename_item.set_sensitive(False) 4681 remove_item.set_sensitive(False) 4682 4683 event_button = gtkgui_helpers.get_possible_button_event(event) 4684 4685 menu.attach_to_widget(self.tree, None) 4686 menu.connect('selection-done', gtkgui_helpers.destroy_widget) 4687 menu.show_all() 4688 menu.popup(None, None, None, None, event_button, event.time) 4689 4690 def make_contact_menu(self, event, titer): 4691 """ 4692 Make contact's popup menu 4693 """ 4694 model = self.modelfilter 4695 jid = model[titer][Column.JID] 4696 account = model[titer][Column.ACCOUNT] 4697 contact = app.contacts.get_contact_with_highest_priority(account, jid) 4698 menu = gui_menu_builder.get_contact_menu(contact, account) 4699 event_button = gtkgui_helpers.get_possible_button_event(event) 4700 menu.attach_to_widget(self.tree, None) 4701 menu.popup(None, None, None, None, event_button, event.time) 4702 4703 def make_multiple_contact_menu(self, event, iters): 4704 """ 4705 Make group's popup menu 4706 """ 4707 model = self.modelfilter 4708 list_ = [] # list of (jid, account) tuples 4709 one_account_offline = False 4710 is_blocked = True 4711 blocking_supported = True 4712 for titer in iters: 4713 jid = model[titer][Column.JID] 4714 account = model[titer][Column.ACCOUNT] 4715 if not app.account_is_available(account): 4716 one_account_offline = True 4717 4718 con = app.connections[account] 4719 if not con.get_module('Blocking').supported: 4720 blocking_supported = False 4721 contact = app.contacts.get_contact_with_highest_priority( 4722 account, jid) 4723 if not helpers.jid_is_blocked(account, jid): 4724 is_blocked = False 4725 list_.append((contact, account)) 4726 4727 menu = Gtk.Menu() 4728 account = None 4729 for (contact, current_account) in list_: 4730 # check that we use the same account for every sender 4731 if account is not None and account != current_account: 4732 account = None 4733 break 4734 account = current_account 4735 show_bookmarked = True 4736 for (contact, current_account) in list_: 4737 # Check that all contacts support direct NUC invite 4738 if not contact.supports(Namespace.CONFERENCE): 4739 show_bookmarked = False 4740 break 4741 if account is not None: 4742 send_group_message_item = Gtk.MenuItem.new_with_mnemonic( 4743 _('Send Group M_essage')) 4744 menu.append(send_group_message_item) 4745 send_group_message_item.connect('activate', 4746 self.on_send_single_message_menuitem_activate, account, list_) 4747 4748 # Invite to Groupchat 4749 invite_item = Gtk.MenuItem.new_with_mnemonic(_('In_vite to')) 4750 4751 gui_menu_builder.build_invite_submenu(invite_item, list_, 4752 show_bookmarked=show_bookmarked) 4753 menu.append(invite_item) 4754 4755 item = Gtk.SeparatorMenuItem.new() # separator 4756 menu.append(item) 4757 4758 # Manage Transport submenu 4759 item = Gtk.MenuItem.new_with_mnemonic(_('_Manage Contacts')) 4760 manage_contacts_submenu = Gtk.Menu() 4761 item.set_submenu(manage_contacts_submenu) 4762 menu.append(item) 4763 4764 # Edit Groups 4765 edit_groups_item = Gtk.MenuItem.new_with_mnemonic(_('Edit _Groups…')) 4766 manage_contacts_submenu.append(edit_groups_item) 4767 edit_groups_item.connect('activate', self.on_edit_groups, list_) 4768 4769 item = Gtk.SeparatorMenuItem.new() # separator 4770 manage_contacts_submenu.append(item) 4771 4772 # Block 4773 if is_blocked and blocking_supported: 4774 unblock_menuitem = Gtk.MenuItem.new_with_mnemonic(_('_Unblock')) 4775 unblock_menuitem.connect('activate', self.on_unblock, list_) 4776 manage_contacts_submenu.append(unblock_menuitem) 4777 else: 4778 block_menuitem = Gtk.MenuItem.new_with_mnemonic(_('_Block')) 4779 block_menuitem.connect('activate', self.on_block, list_) 4780 manage_contacts_submenu.append(block_menuitem) 4781 4782 if not blocking_supported: 4783 block_menuitem.set_sensitive(False) 4784 4785 # Remove 4786 remove_item = Gtk.MenuItem.new_with_mnemonic(_('_Remove')) 4787 manage_contacts_submenu.append(remove_item) 4788 remove_item.connect('activate', self.on_req_usub, list_) 4789 # unsensitive remove if one account is not connected 4790 if one_account_offline: 4791 remove_item.set_sensitive(False) 4792 4793 event_button = gtkgui_helpers.get_possible_button_event(event) 4794 4795 menu.attach_to_widget(self.tree, None) 4796 menu.connect('selection-done', gtkgui_helpers.destroy_widget) 4797 menu.show_all() 4798 menu.popup(None, None, None, None, event_button, event.time) 4799 4800 def make_transport_menu(self, event, titer): 4801 """ 4802 Make transport's popup menu 4803 """ 4804 model = self.modelfilter 4805 jid = model[titer][Column.JID] 4806 account = model[titer][Column.ACCOUNT] 4807 contact = app.contacts.get_contact_with_highest_priority(account, jid) 4808 menu = gui_menu_builder.get_transport_menu(contact, account) 4809 event_button = gtkgui_helpers.get_possible_button_event(event) 4810 menu.attach_to_widget(self.tree, None) 4811 menu.popup(None, None, None, None, event_button, event.time) 4812 4813 def make_groupchat_menu(self, event, titer): 4814 model = self.modelfilter 4815 4816 jid = model[titer][Column.JID] 4817 account = model[titer][Column.ACCOUNT] 4818 contact = app.contacts.get_contact_with_highest_priority(account, jid) 4819 menu = Gtk.Menu() 4820 4821 if jid in app.interface.minimized_controls[account]: 4822 maximize_menuitem = Gtk.MenuItem.new_with_mnemonic(_( 4823 '_Maximize')) 4824 maximize_menuitem.connect('activate', self.on_groupchat_maximized, \ 4825 jid, account) 4826 menu.append(maximize_menuitem) 4827 4828 rename_menuitem = Gtk.MenuItem.new_with_mnemonic(_('Re_name')) 4829 rename_menuitem.connect('activate', 4830 self.on_groupchat_rename, 4831 jid, 4832 account) 4833 menu.append(rename_menuitem) 4834 4835 disconnect_menuitem = Gtk.MenuItem.new_with_mnemonic(_( 4836 '_Leave')) 4837 disconnect_menuitem.connect('activate', self.on_disconnect, jid, 4838 account) 4839 menu.append(disconnect_menuitem) 4840 4841 item = Gtk.SeparatorMenuItem.new() # separator 4842 menu.append(item) 4843 4844 adhoc_menuitem = Gtk.MenuItem.new_with_mnemonic(_('Execute command')) 4845 adhoc_menuitem.connect('activate', self.on_execute_command, contact, 4846 account) 4847 menu.append(adhoc_menuitem) 4848 4849 item = Gtk.SeparatorMenuItem.new() # separator 4850 menu.append(item) 4851 4852 history_menuitem = Gtk.MenuItem.new_with_mnemonic(_('_History')) 4853 history_menuitem.set_action_name('app.browse-history') 4854 dict_ = {'jid': GLib.Variant('s', contact.jid), 4855 'account': GLib.Variant('s', account)} 4856 variant = GLib.Variant('a{sv}', dict_) 4857 history_menuitem.set_action_target_value(variant) 4858 4859 menu.append(history_menuitem) 4860 4861 event_button = gtkgui_helpers.get_possible_button_event(event) 4862 4863 menu.attach_to_widget(self.tree, None) 4864 menu.connect('selection-done', gtkgui_helpers.destroy_widget) 4865 menu.show_all() 4866 menu.popup(None, None, None, None, event_button, event.time) 4867 4868 def show_appropriate_context_menu(self, event, iters): 4869 # iters must be all of the same type 4870 model = self.modelfilter 4871 type_ = model[iters[0]][Column.TYPE] 4872 for titer in iters[1:]: 4873 if model[titer][Column.TYPE] != type_: 4874 return 4875 if type_ == 'group': 4876 self.make_group_menu(event, iters) 4877 if type_ == 'groupchat' and len(iters) == 1: 4878 self.make_groupchat_menu(event, iters[0]) 4879 elif type_ == 'agent' and len(iters) == 1: 4880 self.make_transport_menu(event, iters[0]) 4881 elif type_ in ('contact', 'self_contact') and len(iters) == 1: 4882 self.make_contact_menu(event, iters[0]) 4883 elif type_ == 'contact': 4884 self.make_multiple_contact_menu(event, iters) 4885 elif type_ == 'account' and len(iters) == 1: 4886 self.make_account_menu(event, iters[0]) 4887 4888 def show_treeview_menu(self, event): 4889 try: 4890 model, list_of_paths = self.tree.get_selection().get_selected_rows() 4891 except TypeError: 4892 self.tree.get_selection().unselect_all() 4893 return 4894 if not list_of_paths: 4895 # no row is selected 4896 return 4897 if len(list_of_paths) > 1: 4898 iters = [] 4899 for path in list_of_paths: 4900 iters.append(model.get_iter(path)) 4901 else: 4902 path = list_of_paths[0] 4903 iters = [model.get_iter(path)] 4904 self.show_appropriate_context_menu(event, iters) 4905 4906 return True 4907 4908 def fill_column(self, col): 4909 for rend in self.renderers_list: 4910 col.pack_start(rend[1], rend[2]) 4911 if rend[0] != 'avatar': 4912 col.add_attribute(rend[1], rend[3], rend[4]) 4913 col.set_cell_data_func(rend[1], rend[5], rend[6]) 4914 # set renderers properties 4915 for renderer in self.renderers_propertys: 4916 renderer.set_property(self.renderers_propertys[renderer][0], 4917 self.renderers_propertys[renderer][1]) 4918 4919 def query_tooltip(self, widget, x_pos, y_pos, _keyboard_mode, tooltip): 4920 try: 4921 path = widget.get_path_at_pos(x_pos, y_pos) 4922 row = path[0] 4923 col = path[1] 4924 except TypeError: 4925 self._roster_tooltip.clear_tooltip() 4926 return False 4927 if not row: 4928 self._roster_tooltip.clear_tooltip() 4929 return False 4930 4931 iter_ = None 4932 try: 4933 model = widget.get_model() 4934 iter_ = model.get_iter(row) 4935 except Exception: 4936 self._roster_tooltip.clear_tooltip() 4937 return False 4938 4939 typ = model[iter_][Column.TYPE] 4940 account = model[iter_][Column.ACCOUNT] 4941 jid = model[iter_][Column.JID] 4942 connected_contacts = [] 4943 4944 if typ == 'group': 4945 if jid == _('Observers'): 4946 widget.set_tooltip_cell(tooltip, row, col, None) 4947 tooltip.set_text( 4948 _('Observers can see your status, but you ' 4949 'are not allowed to see theirs')) 4950 return True 4951 return False 4952 4953 if typ in ('contact', 'self_contact'): 4954 contacts = app.contacts.get_contacts(account, jid) 4955 4956 for contact in contacts: 4957 if contact.show not in ('offline', 'error'): 4958 connected_contacts.append(contact) 4959 if not connected_contacts: 4960 # no connected contacts, show the offline one 4961 connected_contacts = contacts 4962 elif typ == 'groupchat': 4963 connected_contacts = app.contacts.get_contacts(account, jid) 4964 elif typ != 'account': 4965 return False 4966 4967 value, widget = self._roster_tooltip.get_tooltip( 4968 row, connected_contacts, account, typ) 4969 tooltip.set_custom(widget) 4970 return value 4971 4972 def add_actions(self): 4973 4974 actions = [ 4975 ('show-roster', 4976 not self.xml.get_object('roster_vbox2').get_no_show_all(), 4977 self.on_show_roster_action), 4978 4979 ('show-offline', 4980 app.settings.get('showoffline'), 4981 self.on_show_offline_contacts_action), 4982 4983 ('show-active', 4984 app.settings.get('show_only_chat_and_online'), 4985 self.on_show_active_contacts_action), 4986 4987 ('show-transports', 4988 app.settings.get('show_transports_group'), 4989 self.on_show_transports_action), 4990 ] 4991 4992 for action in actions: 4993 action_name, variant, func = action 4994 act = Gio.SimpleAction.new_stateful( 4995 action_name, None, GLib.Variant.new_boolean(variant)) 4996 act.connect('change-state', func) 4997 self.window.add_action(act) 4998 4999################################################################################ 5000### 5001################################################################################ 5002 5003 def __init__(self, application): 5004 self.application = application 5005 self.filtering = False 5006 self.starting = False 5007 self.starting_filtering = False 5008 # Number of renderers plugins added 5009 self.nb_ext_renderers = 0 5010 # When we quit, remember if we already saved config once 5011 self.save_done = False 5012 5013 # [icon, name, type, jid, account, editable, mood_pixbuf, 5014 # activity_pixbuf, TUNE_ICON, LOCATION_ICON, avatar_img, 5015 # padlock_pixbuf, visible] 5016 self.columns = [str, str, str, str, str, str, str, str, str, 5017 Gtk.Image, str, bool] 5018 5019 self.xml = get_builder('roster_window.ui') 5020 self.window = self.xml.get_object('roster_window') 5021 application.add_window(self.window) 5022 self.add_actions() 5023 self.hpaned = self.xml.get_object('roster_hpaned') 5024 5025 app.interface.msg_win_mgr = MessageWindowMgr(self.window, self.hpaned) 5026 app.interface.msg_win_mgr.connect('window-delete', 5027 self.on_message_window_delete) 5028 5029 self.advanced_menus = [] # We keep them to destroy them 5030 if app.settings.get('roster_window_skip_taskbar'): 5031 self.window.set_property('skip-taskbar-hint', True) 5032 self.tree = self.xml.get_object('roster_treeview') 5033 sel = self.tree.get_selection() 5034 sel.set_mode(Gtk.SelectionMode.MULTIPLE) 5035 # sel.connect('changed', 5036 # self.on_treeview_selection_changed) 5037 5038 self._iters = {} 5039 # for merged mode 5040 self._iters['MERGED'] = {'account': None, 'groups': {}} 5041 # holds a list of (jid, account) tuples 5042 self._last_selected_contact = [] 5043 self.transports_state_images = {'16': {}, '32': {}, 'opened': {}, 5044 'closed': {}} 5045 5046 self.last_save_dir = None 5047 self.editing_path = None # path of row with cell in edit mode 5048 self.add_new_contact_handler_id = False 5049 self.service_disco_handler_id = False 5050 self.new_chat_menuitem_handler_id = False 5051 self.single_message_menuitem_handler_id = False 5052 self.profile_avatar_menuitem_handler_id = False 5053 #FIXME: When list_accel_closures will be wrapped in pygtk 5054 # no need of this variable 5055 self.have_new_chat_accel = False # Is the "Ctrl+N" shown ? 5056 self.regroup = app.settings.get('mergeaccounts') 5057 self.clicked_path = None # Used remember on which row we clicked 5058 if len(app.connections) < 2: 5059 # Do not merge accounts if only one exists 5060 self.regroup = False 5061 resize_window(self.window, 5062 app.settings.get('roster_width'), 5063 app.settings.get('roster_height')) 5064 restore_roster_position(self.window) 5065 5066 # Remove contact from roster when last event opened 5067 # { (contact, account): { backend: boolean } 5068 self.contacts_to_be_removed = {} 5069 app.events.event_removed_subscribe(self.on_event_removed) 5070 5071 # when this value become 0 we quit main application. If it's more than 0 5072 # it means we are waiting for this number of accounts to disconnect 5073 # before quitting 5074 self.quit_on_next_offline = -1 5075 5076 # groups to draw next time we draw groups. 5077 self.groups_to_draw = {} 5078 # accounts to draw next time we draw accounts. 5079 self.accounts_to_draw = [] 5080 5081 # Status selector 5082 self._status_selector = StatusSelector() 5083 self.xml.roster_vbox2.add(self._status_selector) 5084 5085 # Enable/Disable checkboxes at start 5086 if app.settings.get('showoffline'): 5087 self.window.lookup_action('show-active').set_enabled(False) 5088 5089 if app.settings.get('show_only_chat_and_online'): 5090 self.window.lookup_action('show-offline').set_enabled(False) 5091 5092 if self.hpaned.get_child2() is None: 5093 self.window.lookup_action('show-roster').set_enabled(False) 5094 5095 # columns 5096 col = Gtk.TreeViewColumn() 5097 # list of renderers with attributes / properties in the form: 5098 # (name, renderer_object, expand?, attribute_name, attribute_value, 5099 # cell_data_func, func_arg) 5100 self.renderers_list = [] 5101 self.renderers_propertys = {} 5102 5103 renderer_text = Gtk.CellRendererText() 5104 self.renderers_propertys[renderer_text] = ('ellipsize', 5105 Pango.EllipsizeMode.END) 5106 5107 def add_avatar_renderer(): 5108 self.renderers_list.append(('avatar', Gtk.CellRendererPixbuf(), 5109 False, None, Column.AVATAR_IMG, 5110 self._fill_avatar_pixbuf_renderer, None)) 5111 5112 if app.settings.get('avatar_position_in_roster') == 'left': 5113 add_avatar_renderer() 5114 5115 self.renderers_list += ( 5116 ('icon', Gtk.CellRendererPixbuf(), False, 5117 'icon_name', Column.IMG, self._iconCellDataFunc, None), 5118 5119 ('name', renderer_text, True, 5120 'markup', Column.NAME, self._nameCellDataFunc, None), 5121 5122 ('mood', Gtk.CellRendererPixbuf(), False, 5123 'icon_name', Column.MOOD_PIXBUF, 5124 self._fill_pep_pixbuf_renderer, Column.MOOD_PIXBUF), 5125 5126 ('activity', Gtk.CellRendererPixbuf(), False, 5127 'icon_name', Column.ACTIVITY_PIXBUF, 5128 self._fill_pep_pixbuf_renderer, Column.ACTIVITY_PIXBUF), 5129 5130 ('tune', Gtk.CellRendererPixbuf(), False, 5131 'icon_name', Column.TUNE_ICON, 5132 self._fill_pep_pixbuf_renderer, Column.TUNE_ICON), 5133 5134 ('geoloc', Gtk.CellRendererPixbuf(), False, 5135 'icon_name', Column.LOCATION_ICON, 5136 self._fill_pep_pixbuf_renderer, Column.LOCATION_ICON)) 5137 5138 if app.settings.get('avatar_position_in_roster') == 'right': 5139 add_avatar_renderer() 5140 5141 self.renderers_list.append(('padlock', Gtk.CellRendererPixbuf(), False, 5142 'icon_name', Column.PADLOCK_PIXBUF, 5143 self._fill_padlock_pixbuf_renderer, None)) 5144 5145 # fill and append column 5146 self.fill_column(col) 5147 self.tree.append_column(col) 5148 5149 # do not show gtk arrows workaround 5150 col = Gtk.TreeViewColumn() 5151 render_pixbuf = Gtk.CellRendererPixbuf() 5152 col.pack_start(render_pixbuf, False) 5153 self.tree.append_column(col) 5154 col.set_visible(False) 5155 self.tree.set_expander_column(col) 5156 5157 # Signals 5158 # Drag 5159 self.tree.enable_model_drag_source( 5160 Gdk.ModifierType.BUTTON1_MASK, 5161 [], 5162 Gdk.DragAction.DEFAULT | 5163 Gdk.DragAction.MOVE | 5164 Gdk.DragAction.COPY) 5165 self.tree.drag_source_add_text_targets() 5166 5167 # Drop 5168 self.tree.enable_model_drag_dest([], Gdk.DragAction.DEFAULT) 5169 self.TARGET_TYPE_URI_LIST = 80 5170 uri_entry = Gtk.TargetEntry.new( 5171 'text/uri-list', 5172 Gtk.TargetFlags.OTHER_APP, 5173 self.TARGET_TYPE_URI_LIST) 5174 dst_targets = Gtk.TargetList.new([uri_entry]) 5175 dst_targets.add_text_targets(0) 5176 self.tree.drag_dest_set_target_list(dst_targets) 5177 5178 # Connect 5179 self.tree.connect('drag-begin', self.drag_begin) 5180 self.tree.connect('drag-end', self.drag_end) 5181 self.tree.connect('drag-drop', self.drag_drop) 5182 self.tree.connect('drag-data-get', self.drag_data_get_data) 5183 self.tree.connect('drag-data-received', self.drag_data_received_data) 5184 self.dragging = False 5185 self.xml.connect_signals(self) 5186 self.combobox_callback_active = True 5187 5188 self.collapsed_rows = app.settings.get('collapsed_rows').split('\t') 5189 self.tree.set_has_tooltip(True) 5190 self._roster_tooltip = RosterTooltip() 5191 self.tree.connect('query-tooltip', self.query_tooltip) 5192 # Workaround: For strange reasons signal is behaving like row-changed 5193 self._toggeling_row = False 5194 self.setup_and_draw_roster() 5195 5196 if app.settings.get('show_roster_on_startup') == 'always': 5197 self.window.show_all() 5198 elif app.settings.get('show_roster_on_startup') == 'never': 5199 if app.settings.get('trayicon') != 'always': 5200 # Without trayicon, user should see the roster! 5201 self.window.show_all() 5202 app.settings.set('last_roster_visible', True) 5203 else: 5204 if app.settings.get('last_roster_visible') or \ 5205 app.settings.get('trayicon') != 'always': 5206 self.window.show_all() 5207 5208 self.scale_factor = self.window.get_scale_factor() 5209 5210 accounts = app.settings.get_accounts() 5211 5212 if (not accounts or 5213 accounts == ['Local'] and 5214 not app.settings.get_account_setting('Local', 'active')): 5215 # if we have no account configured or only Local account but not enabled 5216 def _open_wizard(): 5217 open_window('AccountWizard') 5218 5219 # Open wizard only after roster is created, so we can make it 5220 # transient for the roster window 5221 GLib.idle_add(_open_wizard) 5222 5223 # Setting CTRL+S to be the shortcut to change status message 5224 accel_group = Gtk.AccelGroup() 5225 keyval, mod = Gtk.accelerator_parse('<Control>s') 5226 accel_group.connect(keyval, mod, Gtk.AccelFlags.VISIBLE, 5227 self.accel_group_func) 5228 5229 # Setting CTRL+k to focus rfilter_entry 5230 keyval, mod = Gtk.accelerator_parse('<Control>k') 5231 accel_group.connect(keyval, mod, Gtk.AccelFlags.VISIBLE, 5232 self.accel_group_func) 5233 self.window.add_accel_group(accel_group) 5234 5235 # Setting the search stuff 5236 self.rfilter_entry = self.xml.get_object('rfilter_entry') 5237 self.rfilter_string = '' 5238 self.rfilter_enabled = False 5239 self.rfilter_entry.connect('key-press-event', 5240 self.on_rfilter_entry_key_press_event) 5241 5242 app.ged.register_event_handler('presence-received', ged.GUI1, 5243 self._nec_presence_received) 5244 app.ged.register_event_handler('roster-received', ged.GUI1, 5245 self._nec_roster_received) 5246 app.ged.register_event_handler('anonymous-auth', ged.GUI1, 5247 self._nec_anonymous_auth) 5248 app.ged.register_event_handler('our-show', ged.GUI2, 5249 self._nec_our_show) 5250 app.ged.register_event_handler('connection-type', ged.GUI1, 5251 self._nec_connection_type) 5252 app.ged.register_event_handler('agent-removed', ged.GUI1, 5253 self._nec_agent_removed) 5254 app.ged.register_event_handler('nickname-received', ged.GUI1, 5255 self._on_nickname_received) 5256 app.ged.register_event_handler('mood-received', ged.GUI1, 5257 self._on_mood_received) 5258 app.ged.register_event_handler('activity-received', ged.GUI1, 5259 self._on_activity_received) 5260 app.ged.register_event_handler('tune-received', ged.GUI1, 5261 self._on_tune_received) 5262 app.ged.register_event_handler('location-received', ged.GUI1, 5263 self._on_location_received) 5264 app.ged.register_event_handler('update-roster-avatar', ged.GUI1, 5265 self._nec_update_avatar) 5266 app.ged.register_event_handler('update-room-avatar', ged.GUI1, 5267 self._nec_update_avatar) 5268 app.ged.register_event_handler('muc-subject', ged.GUI1, 5269 self._nec_muc_subject_received) 5270 app.ged.register_event_handler('metacontacts-received', ged.GUI2, 5271 self._nec_metacontacts_received) 5272 app.ged.register_event_handler('signed-in', ged.GUI1, 5273 self._nec_signed_in) 5274 app.ged.register_event_handler('decrypted-message-received', ged.GUI2, 5275 self._nec_decrypted_message_received) 5276 app.ged.register_event_handler('blocking', ged.GUI1, 5277 self._nec_blocking) 5278 app.ged.register_event_handler('style-changed', ged.GUI1, 5279 self._style_changed) 5280 app.ged.register_event_handler('chatstate-received', ged.GUI1, 5281 self._nec_chatstate_received) 5282 app.ged.register_event_handler('muc-disco-update', ged.GUI1, 5283 self._on_muc_disco_update) 5284 app.ged.register_event_handler('bookmarks-received', ged.GUI2, 5285 self._on_bookmarks_received) 5286