5286 lines
211 KiB
Python
5286 lines
211 KiB
Python
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
|
|
# Copyright (C) 2005 Alex Mauer <hawke AT hawkesnest.net>
|
|
# Stéphan Kochen <stephan AT kochen.nl>
|
|
# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
|
|
# Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
|
|
# Nikos Kouremenos <kourem AT gmail.com>
|
|
# Copyright (C) 2006 Stefan Bethge <stefan AT lanpartei.de>
|
|
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
|
|
# Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
|
|
# James Newton <redshodan AT gmail.com>
|
|
# Tomasz Melcer <liori AT exroot.org>
|
|
# Julien Pivotto <roidelapluie AT gmail.com>
|
|
# Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
|
|
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
|
|
# Jonathan Schleifer <js-gajim AT webkeks.org>
|
|
#
|
|
# This file is part of Gajim.
|
|
#
|
|
# Gajim is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published
|
|
# by the Free Software Foundation; version 3 only.
|
|
#
|
|
# Gajim is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import locale
|
|
import logging
|
|
from enum import IntEnum, unique
|
|
|
|
from gi.repository import Gtk
|
|
from gi.repository import Gdk
|
|
from gi.repository import Pango
|
|
from gi.repository import GObject
|
|
from gi.repository import GLib
|
|
from gi.repository import Gio
|
|
from nbxmpp.namespaces import Namespace
|
|
|
|
from gajim import dialogs
|
|
from gajim import vcard
|
|
from gajim import gtkgui_helpers
|
|
from gajim import gui_menu_builder
|
|
|
|
from gajim.common import app
|
|
from gajim.common import helpers
|
|
from gajim.common.exceptions import GajimGeneralException
|
|
from gajim.common import i18n
|
|
from gajim.common.helpers import save_roster_position
|
|
from gajim.common.helpers import ask_for_status_message
|
|
from gajim.common.i18n import _
|
|
from gajim.common.const import PEPEventType, AvatarSize, StyleAttr
|
|
from gajim.common.dbus import location
|
|
|
|
from gajim.common import ged
|
|
from gajim.message_window import MessageWindowMgr
|
|
|
|
from gajim.gui.dialogs import DialogButton
|
|
from gajim.gui.dialogs import ConfirmationDialog
|
|
from gajim.gui.dialogs import ConfirmationCheckDialog
|
|
from gajim.gui.dialogs import ErrorDialog
|
|
from gajim.gui.dialogs import InputDialog
|
|
from gajim.gui.dialogs import InformationDialog
|
|
from gajim.gui.single_message import SingleMessageWindow
|
|
from gajim.gui.add_contact import AddNewContactWindow
|
|
from gajim.gui.service_registration import ServiceRegistration
|
|
from gajim.gui.discovery import ServiceDiscoveryWindow
|
|
from gajim.gui.tooltips import RosterTooltip
|
|
from gajim.gui.adhoc import AdHocCommand
|
|
from gajim.gui.status_selector import StatusSelector
|
|
from gajim.gui.util import get_icon_name
|
|
from gajim.gui.util import resize_window
|
|
from gajim.gui.util import restore_roster_position
|
|
from gajim.gui.util import get_metacontact_surface
|
|
from gajim.gui.util import get_builder
|
|
from gajim.gui.util import set_urgency_hint
|
|
from gajim.gui.util import get_activity_icon_name
|
|
from gajim.gui.util import get_account_activity_icon_name
|
|
from gajim.gui.util import get_account_mood_icon_name
|
|
from gajim.gui.util import get_account_tune_icon_name
|
|
from gajim.gui.util import get_account_location_icon_name
|
|
from gajim.gui.util import open_window
|
|
|
|
|
|
log = logging.getLogger('gajim.roster')
|
|
|
|
@unique
|
|
class Column(IntEnum):
|
|
IMG = 0 # image to show state (online, new message etc)
|
|
NAME = 1 # cellrenderer text that holds contact nickname
|
|
TYPE = 2 # account, group or contact?
|
|
JID = 3 # the jid of the row
|
|
ACCOUNT = 4 # cellrenderer text that holds account name
|
|
MOOD_PIXBUF = 5
|
|
ACTIVITY_PIXBUF = 6
|
|
TUNE_ICON = 7
|
|
LOCATION_ICON = 8
|
|
AVATAR_IMG = 9 # avatar_sha
|
|
PADLOCK_PIXBUF = 10 # use for account row only
|
|
VISIBLE = 11
|
|
|
|
|
|
class RosterWindow:
|
|
"""
|
|
Class for main window of the GTK interface
|
|
"""
|
|
|
|
def _get_account_iter(self, name, model=None):
|
|
"""
|
|
Return the Gtk.TreeIter of the given account or None if not found
|
|
|
|
Keyword arguments:
|
|
name -- the account name
|
|
model -- the data model (default TreeFilterModel)
|
|
"""
|
|
if model is None:
|
|
model = self.modelfilter
|
|
if model is None:
|
|
return
|
|
|
|
if self.regroup:
|
|
name = 'MERGED'
|
|
if name not in self._iters:
|
|
return None
|
|
it = self._iters[name]['account']
|
|
|
|
if model == self.model or it is None:
|
|
return it
|
|
try:
|
|
(ok, it) = self.modelfilter.convert_child_iter_to_iter(it)
|
|
if ok:
|
|
return it
|
|
return None
|
|
except RuntimeError:
|
|
return None
|
|
|
|
|
|
def _get_group_iter(self, name, account, model=None):
|
|
"""
|
|
Return the Gtk.TreeIter of the given group or None if not found
|
|
|
|
Keyword arguments:
|
|
name -- the group name
|
|
account -- the account name
|
|
model -- the data model (default TreeFilterModel)
|
|
"""
|
|
if model is None:
|
|
model = self.modelfilter
|
|
if model is None:
|
|
return
|
|
|
|
if self.regroup:
|
|
account = 'MERGED'
|
|
|
|
if account not in self._iters:
|
|
return None
|
|
if name not in self._iters[account]['groups']:
|
|
return None
|
|
|
|
it = self._iters[account]['groups'][name]
|
|
if model == self.model or it is None:
|
|
return it
|
|
try:
|
|
(ok, it) = self.modelfilter.convert_child_iter_to_iter(it)
|
|
if ok:
|
|
return it
|
|
return None
|
|
except RuntimeError:
|
|
return None
|
|
|
|
|
|
def _get_self_contact_iter(self, account, model=None):
|
|
"""
|
|
Return the Gtk.TreeIter of SelfContact or None if not found
|
|
|
|
Keyword arguments:
|
|
account -- the account of SelfContact
|
|
model -- the data model (default TreeFilterModel)
|
|
"""
|
|
jid = app.get_jid_from_account(account)
|
|
its = self._get_contact_iter(jid, account, model=model)
|
|
if its:
|
|
return its[0]
|
|
return None
|
|
|
|
|
|
def _get_contact_iter(self, jid, account, contact=None, model=None):
|
|
"""
|
|
Return a list of Gtk.TreeIter of the given contact
|
|
|
|
Keyword arguments:
|
|
jid -- the jid without resource
|
|
account -- the account
|
|
contact -- the contact (default None)
|
|
model -- the data model (default TreeFilterModel)
|
|
"""
|
|
if model is None:
|
|
model = self.modelfilter
|
|
# when closing Gajim model can be none (async pbs?)
|
|
if model is None:
|
|
return []
|
|
|
|
if not contact:
|
|
contact = app.contacts.get_first_contact_from_jid(account, jid)
|
|
if not contact:
|
|
# We don't know this contact
|
|
return []
|
|
|
|
if account not in self._iters:
|
|
return []
|
|
|
|
if jid not in self._iters[account]['contacts']:
|
|
return []
|
|
|
|
its = self._iters[account]['contacts'][jid]
|
|
|
|
if not its:
|
|
return []
|
|
|
|
if model == self.model:
|
|
return its
|
|
|
|
its2 = []
|
|
for it in its:
|
|
try:
|
|
(ok, it) = self.modelfilter.convert_child_iter_to_iter(it)
|
|
if ok:
|
|
its2.append(it)
|
|
except RuntimeError:
|
|
pass
|
|
return its2
|
|
|
|
@staticmethod
|
|
def _iter_is_separator(model, titer):
|
|
"""
|
|
Return True if the given iter is a separator
|
|
|
|
Keyword arguments:
|
|
model -- the data model
|
|
iter -- the Gtk.TreeIter to test
|
|
"""
|
|
if model[titer][0] == 'SEPARATOR':
|
|
return True
|
|
return False
|
|
|
|
#############################################################################
|
|
### Methods for adding and removing roster window items
|
|
#############################################################################
|
|
|
|
def add_account(self, account):
|
|
"""
|
|
Add account to roster and draw it. Do nothing if it is already in
|
|
"""
|
|
if self._get_account_iter(account):
|
|
# Will happen on reconnect or for merged accounts
|
|
return
|
|
|
|
if self.regroup:
|
|
# Merged accounts view
|
|
show = helpers.get_global_show()
|
|
it = self.model.append(None, [get_icon_name(show),
|
|
_('Merged accounts'), 'account', '', 'all', None, None, None,
|
|
None, None, None, True] + [None] * self.nb_ext_renderers)
|
|
self._iters['MERGED']['account'] = it
|
|
else:
|
|
show = helpers.get_connection_status(account)
|
|
our_jid = app.get_jid_from_account(account)
|
|
|
|
it = self.model.append(None, [get_icon_name(show),
|
|
GLib.markup_escape_text(account), 'account', our_jid,
|
|
account, None, None, None, None, None, None, True] +
|
|
[None] * self.nb_ext_renderers)
|
|
self._iters[account]['account'] = it
|
|
|
|
self.draw_account(account)
|
|
|
|
|
|
def add_account_contacts(self, account, improve_speed=True,
|
|
draw_contacts=True):
|
|
"""
|
|
Add all contacts and groups of the given account to roster, draw them
|
|
and account
|
|
"""
|
|
if improve_speed:
|
|
self._before_fill()
|
|
jids = app.contacts.get_jid_list(account)
|
|
|
|
for jid in jids:
|
|
self.add_contact(jid, account)
|
|
|
|
if draw_contacts:
|
|
# Do not freeze the GUI when drawing the contacts
|
|
if jids:
|
|
# Overhead is big, only invoke when needed
|
|
self._idle_draw_jids_of_account(jids, account)
|
|
|
|
# Draw all known groups
|
|
for group in app.groups[account]:
|
|
self.draw_group(group, account)
|
|
self.draw_account(account)
|
|
|
|
if improve_speed:
|
|
self._after_fill()
|
|
|
|
def _add_group_iter(self, account, group):
|
|
"""
|
|
Add a group iter in roster and return the newly created iter
|
|
"""
|
|
if self.regroup:
|
|
account_group = 'MERGED'
|
|
else:
|
|
account_group = account
|
|
delimiter = app.connections[account].get_module('Delimiter').delimiter
|
|
group_splited = group.split(delimiter)
|
|
parent_group = delimiter.join(group_splited[:-1])
|
|
if len(group_splited) > 1 and parent_group in self._iters[account_group]['groups']:
|
|
iter_parent = self._iters[account_group]['groups'][parent_group]
|
|
elif parent_group:
|
|
iter_parent = self._add_group_iter(account, parent_group)
|
|
if parent_group not in app.groups[account]:
|
|
if account + parent_group in self.collapsed_rows:
|
|
is_expanded = False
|
|
else:
|
|
is_expanded = True
|
|
app.groups[account][parent_group] = {'expand': is_expanded}
|
|
else:
|
|
iter_parent = self._get_account_iter(account, self.model)
|
|
iter_group = self.model.append(iter_parent,
|
|
[get_icon_name('closed'),
|
|
GLib.markup_escape_text(group), 'group', group, account, None,
|
|
None, None, None, None, None, False] + [None] * self.nb_ext_renderers)
|
|
self.draw_group(group, account)
|
|
self._iters[account_group]['groups'][group] = iter_group
|
|
return iter_group
|
|
|
|
def _add_entity(self, contact, account, groups=None,
|
|
big_brother_contact=None, big_brother_account=None):
|
|
"""
|
|
Add the given contact to roster data model
|
|
|
|
Contact is added regardless if he is already in roster or not. Return
|
|
list of newly added iters.
|
|
|
|
Keyword arguments:
|
|
contact -- the contact to add
|
|
account -- the contacts account
|
|
groups -- list of groups to add the contact to.
|
|
(default groups in contact.get_shown_groups()).
|
|
Parameter ignored when big_brother_contact is specified.
|
|
big_brother_contact -- if specified contact is added as child
|
|
big_brother_contact. (default None)
|
|
"""
|
|
added_iters = []
|
|
visible = self.contact_is_visible(contact, account)
|
|
if big_brother_contact:
|
|
# Add contact under big brother
|
|
|
|
parent_iters = self._get_contact_iter(
|
|
big_brother_contact.jid, big_brother_account,
|
|
big_brother_contact, self.model)
|
|
|
|
# Do not confuse get_contact_iter: Sync groups of family members
|
|
contact.groups = big_brother_contact.groups[:]
|
|
|
|
image = self._get_avatar_image(account, contact.jid)
|
|
|
|
for child_iter in parent_iters:
|
|
it = self.model.append(child_iter, [None,
|
|
contact.get_shown_name(), 'contact', contact.jid, account,
|
|
None, None, None, None, image, None, visible] + \
|
|
[None] * self.nb_ext_renderers)
|
|
added_iters.append(it)
|
|
if contact.jid in self._iters[account]['contacts']:
|
|
self._iters[account]['contacts'][contact.jid].append(it)
|
|
else:
|
|
self._iters[account]['contacts'][contact.jid] = [it]
|
|
else:
|
|
# We are a normal contact. Add us to our groups.
|
|
if not groups:
|
|
groups = contact.get_shown_groups()
|
|
for group in groups:
|
|
child_iterG = self._get_group_iter(group, account,
|
|
model=self.model)
|
|
if not child_iterG:
|
|
# Group is not yet in roster, add it!
|
|
child_iterG = self._add_group_iter(account, group)
|
|
|
|
if contact.is_transport():
|
|
typestr = 'agent'
|
|
elif contact.is_groupchat:
|
|
typestr = 'groupchat'
|
|
else:
|
|
typestr = 'contact'
|
|
|
|
image = self._get_avatar_image(account, contact.jid)
|
|
|
|
# we add some values here. see draw_contact
|
|
# for more
|
|
i_ = self.model.append(child_iterG, [None,
|
|
contact.get_shown_name(), typestr, contact.jid, account,
|
|
None, None, None, None, image, None, visible] + \
|
|
[None] * self.nb_ext_renderers)
|
|
added_iters.append(i_)
|
|
if contact.jid in self._iters[account]['contacts']:
|
|
self._iters[account]['contacts'][contact.jid].append(i_)
|
|
else:
|
|
self._iters[account]['contacts'][contact.jid] = [i_]
|
|
|
|
# Restore the group expand state
|
|
if account + group in self.collapsed_rows:
|
|
is_expanded = False
|
|
else:
|
|
is_expanded = True
|
|
if group not in app.groups[account]:
|
|
app.groups[account][group] = {'expand': is_expanded}
|
|
|
|
return added_iters
|
|
|
|
def _remove_entity(self, contact, account, groups=None):
|
|
"""
|
|
Remove the given contact from roster data model
|
|
|
|
Empty groups after contact removal are removed too.
|
|
Return False if contact still has children and deletion was
|
|
not performed.
|
|
Return True on success.
|
|
|
|
Keyword arguments:
|
|
contact -- the contact to add
|
|
account -- the contacts account
|
|
groups -- list of groups to remove the contact from.
|
|
"""
|
|
iters = self._get_contact_iter(contact.jid, account, contact,
|
|
self.model)
|
|
|
|
parent_iter = self.model.iter_parent(iters[0])
|
|
parent_type = self.model[parent_iter][Column.TYPE]
|
|
|
|
if groups:
|
|
# Only remove from specified groups
|
|
all_iters = iters[:]
|
|
group_iters = [self._get_group_iter(group, account)
|
|
for group in groups]
|
|
iters = [titer for titer in all_iters
|
|
if self.model.iter_parent(titer) in group_iters]
|
|
|
|
iter_children = self.model.iter_children(iters[0])
|
|
|
|
if iter_children:
|
|
# We have children. We cannot be removed!
|
|
return False
|
|
# Remove us and empty groups from the model
|
|
for i in iters:
|
|
parent_i = self.model.iter_parent(i)
|
|
parent_type = self.model[parent_i][Column.TYPE]
|
|
|
|
to_be_removed = i
|
|
while parent_type == 'group' and \
|
|
self.model.iter_n_children(parent_i) == 1:
|
|
if self.regroup:
|
|
account_group = 'MERGED'
|
|
else:
|
|
account_group = account
|
|
group = self.model[parent_i][Column.JID]
|
|
if group in app.groups[account]:
|
|
del app.groups[account][group]
|
|
to_be_removed = parent_i
|
|
del self._iters[account_group]['groups'][group]
|
|
parent_i = self.model.iter_parent(parent_i)
|
|
parent_type = self.model[parent_i][Column.TYPE]
|
|
self.model.remove(to_be_removed)
|
|
|
|
del self._iters[account]['contacts'][contact.jid]
|
|
return True
|
|
|
|
def _add_metacontact_family(self, family, account):
|
|
"""
|
|
Add the give Metacontact family to roster data model
|
|
|
|
Add Big Brother to his groups and all others under him.
|
|
Return list of all added (contact, account) tuples with
|
|
Big Brother as first element.
|
|
|
|
Keyword arguments:
|
|
family -- the family, see Contacts.get_metacontacts_family()
|
|
"""
|
|
|
|
nearby_family, big_brother_jid, big_brother_account = \
|
|
self._get_nearby_family_and_big_brother(family, account)
|
|
if not big_brother_jid:
|
|
return []
|
|
big_brother_contact = app.contacts.get_first_contact_from_jid(
|
|
big_brother_account, big_brother_jid)
|
|
|
|
self._add_entity(big_brother_contact, big_brother_account)
|
|
|
|
brothers = []
|
|
# Filter family members
|
|
for data in nearby_family:
|
|
_account = data['account']
|
|
_jid = data['jid']
|
|
_contact = app.contacts.get_first_contact_from_jid(
|
|
_account, _jid)
|
|
|
|
if not _contact or _contact == big_brother_contact:
|
|
# Corresponding account is not connected
|
|
# or brother already added
|
|
continue
|
|
|
|
self._add_entity(_contact, _account,
|
|
big_brother_contact=big_brother_contact,
|
|
big_brother_account=big_brother_account)
|
|
brothers.append((_contact, _account))
|
|
|
|
brothers.insert(0, (big_brother_contact, big_brother_account))
|
|
return brothers
|
|
|
|
def _remove_metacontact_family(self, family, account):
|
|
"""
|
|
Remove the given Metacontact family from roster data model
|
|
|
|
See Contacts.get_metacontacts_family() and
|
|
RosterWindow._remove_entity()
|
|
"""
|
|
nearby_family = self._get_nearby_family_and_big_brother(
|
|
family, account)[0]
|
|
|
|
# Family might has changed (actual big brother not on top).
|
|
# Remove children first then big brother
|
|
family_in_roster = False
|
|
for data in nearby_family:
|
|
_account = data['account']
|
|
_jid = data['jid']
|
|
_contact = app.contacts.get_first_contact_from_jid(_account, _jid)
|
|
|
|
iters = self._get_contact_iter(_jid, _account, _contact, self.model)
|
|
if not iters or not _contact:
|
|
# Family might not be up to date.
|
|
# Only try to remove what is actually in the roster
|
|
continue
|
|
|
|
family_in_roster = True
|
|
|
|
parent_iter = self.model.iter_parent(iters[0])
|
|
parent_type = self.model[parent_iter][Column.TYPE]
|
|
|
|
if parent_type != 'contact':
|
|
# The contact on top
|
|
old_big_account = _account
|
|
old_big_contact = _contact
|
|
continue
|
|
|
|
self._remove_entity(_contact, _account)
|
|
|
|
if not family_in_roster:
|
|
return False
|
|
|
|
self._remove_entity(old_big_contact, old_big_account)
|
|
|
|
return True
|
|
|
|
def _recalibrate_metacontact_family(self, family, account):
|
|
"""
|
|
Regroup metacontact family if necessary
|
|
"""
|
|
|
|
brothers = []
|
|
nearby_family, big_brother_jid, big_brother_account = \
|
|
self._get_nearby_family_and_big_brother(family, account)
|
|
big_brother_contact = app.contacts.get_contact(big_brother_account,
|
|
big_brother_jid)
|
|
child_iters = self._get_contact_iter(big_brother_jid,
|
|
big_brother_account, model=self.model)
|
|
if child_iters:
|
|
parent_iter = self.model.iter_parent(child_iters[0])
|
|
parent_type = self.model[parent_iter][Column.TYPE]
|
|
|
|
# Check if the current BigBrother has even been before.
|
|
if parent_type == 'contact':
|
|
for data in nearby_family:
|
|
# recalibrate after remove to keep highlight
|
|
if data['jid'] in app.to_be_removed[data['account']]:
|
|
return
|
|
|
|
self._remove_metacontact_family(family, account)
|
|
brothers = self._add_metacontact_family(family, account)
|
|
|
|
for c, acc in brothers:
|
|
self.draw_completely(c.jid, acc)
|
|
|
|
# Check is small brothers are under the big brother
|
|
for child in nearby_family:
|
|
_jid = child['jid']
|
|
_account = child['account']
|
|
if _account == big_brother_account and _jid == big_brother_jid:
|
|
continue
|
|
child_iters = self._get_contact_iter(_jid, _account,
|
|
model=self.model)
|
|
if not child_iters:
|
|
continue
|
|
parent_iter = self.model.iter_parent(child_iters[0])
|
|
parent_type = self.model[parent_iter][Column.TYPE]
|
|
if parent_type != 'contact':
|
|
_contact = app.contacts.get_contact(_account, _jid)
|
|
self._remove_entity(_contact, _account)
|
|
self._add_entity(_contact, _account, groups=None,
|
|
big_brother_contact=big_brother_contact,
|
|
big_brother_account=big_brother_account)
|
|
|
|
def _get_nearby_family_and_big_brother(self, family, account):
|
|
return app.contacts.get_nearby_family_and_big_brother(family, account)
|
|
|
|
def _add_self_contact(self, account):
|
|
"""
|
|
Add account's SelfContact to roster and draw it and the account
|
|
|
|
Return the SelfContact contact instance
|
|
"""
|
|
jid = app.get_jid_from_account(account)
|
|
contact = app.contacts.get_first_contact_from_jid(account, jid)
|
|
|
|
child_iterA = self._get_account_iter(account, self.model)
|
|
self._iters[account]['contacts'][jid] = [self.model.append(child_iterA,
|
|
[None, app.nicks[account], 'self_contact', jid, account, None,
|
|
None, None, None, None, None, True] + [None] * self.nb_ext_renderers)]
|
|
|
|
self.draw_completely(jid, account)
|
|
self.draw_account(account)
|
|
|
|
return contact
|
|
|
|
def redraw_metacontacts(self, account):
|
|
for family in app.contacts.iter_metacontacts_families(account):
|
|
self._recalibrate_metacontact_family(family, account)
|
|
|
|
def add_contact(self, jid, account):
|
|
"""
|
|
Add contact to roster and draw him
|
|
|
|
Add contact to all its group and redraw the groups, the contact and the
|
|
account. If it's a Metacontact, add and draw the whole family.
|
|
Do nothing if the contact is already in roster.
|
|
|
|
Return the added contact instance. If it is a Metacontact return
|
|
Big Brother.
|
|
|
|
Keyword arguments:
|
|
jid -- the contact's jid or SelfJid to add SelfContact
|
|
account -- the corresponding account.
|
|
"""
|
|
contact = app.contacts.get_contact_with_highest_priority(account, jid)
|
|
if self._get_contact_iter(jid, account, contact, self.model):
|
|
# If contact already in roster, do nothing
|
|
return
|
|
|
|
if jid == app.get_jid_from_account(account):
|
|
return self._add_self_contact(account)
|
|
|
|
is_observer = contact.is_observer()
|
|
if is_observer:
|
|
# if he has a tag, remove it
|
|
app.contacts.remove_metacontact(account, jid)
|
|
|
|
# Add contact to roster
|
|
family = app.contacts.get_metacontacts_family(account, jid)
|
|
contacts = []
|
|
if family:
|
|
# We have a family. So we are a metacontact.
|
|
# Add all family members that we shall be grouped with
|
|
if self.regroup:
|
|
# remove existing family members to regroup them
|
|
self._remove_metacontact_family(family, account)
|
|
contacts = self._add_metacontact_family(family, account)
|
|
else:
|
|
# We are a normal contact
|
|
contacts = [(contact, account), ]
|
|
self._add_entity(contact, account)
|
|
|
|
# Draw the contact and its groups contact
|
|
if not self.starting:
|
|
for c, acc in contacts:
|
|
self.draw_completely(c.jid, acc)
|
|
for group in contact.get_shown_groups():
|
|
self.draw_group(group, account)
|
|
self._adjust_group_expand_collapse_state(group, account)
|
|
self.draw_account(account)
|
|
|
|
return contacts[0][0] # it's contact/big brother with highest priority
|
|
|
|
def remove_contact(self, jid, account, force=False, backend=False, maximize=False):
|
|
"""
|
|
Remove contact from roster
|
|
|
|
Remove contact from all its group. Remove empty groups or redraw
|
|
otherwise.
|
|
Draw the account.
|
|
If it's a Metacontact, remove the whole family.
|
|
Do nothing if the contact is not in roster.
|
|
|
|
Keyword arguments:
|
|
jid -- the contact's jid or SelfJid to remove SelfContact
|
|
account -- the corresponding account.
|
|
force -- remove contact even it has pending evens (Default False)
|
|
backend -- also remove contact instance (Default False)
|
|
"""
|
|
contact = app.contacts.get_contact_with_highest_priority(account, jid)
|
|
if not contact:
|
|
return
|
|
|
|
if not force and self.contact_has_pending_roster_events(contact,
|
|
account):
|
|
return False
|
|
|
|
iters = self._get_contact_iter(jid, account, contact, self.model)
|
|
if iters:
|
|
# no more pending events
|
|
# Remove contact from roster directly
|
|
family = app.contacts.get_metacontacts_family(account, jid)
|
|
if family:
|
|
# We have a family. So we are a metacontact.
|
|
self._remove_metacontact_family(family, account)
|
|
else:
|
|
self._remove_entity(contact, account)
|
|
|
|
old_grps = []
|
|
if backend:
|
|
if not app.interface.msg_win_mgr.get_control(jid, account) or \
|
|
force:
|
|
# If a window is still opened: don't remove contact instance
|
|
# Remove contact before redrawing, otherwise the old
|
|
# numbers will still be show
|
|
if not maximize:
|
|
# Don't remove contact when we maximize a room
|
|
app.contacts.remove_jid(account, jid, remove_meta=True)
|
|
if iters:
|
|
rest_of_family = [data for data in family
|
|
if account != data['account'] or jid != data['jid']]
|
|
if rest_of_family:
|
|
# reshow the rest of the family
|
|
brothers = self._add_metacontact_family(rest_of_family,
|
|
account)
|
|
for c, acc in brothers:
|
|
self.draw_completely(c.jid, acc)
|
|
else:
|
|
for c in app.contacts.get_contacts(account, jid):
|
|
c.sub = 'none'
|
|
c.show = 'not in roster'
|
|
c.status = ''
|
|
old_grps = c.get_shown_groups()
|
|
c.groups = [_('Not in contact list')]
|
|
self._add_entity(c, account)
|
|
self.draw_contact(jid, account)
|
|
|
|
if iters:
|
|
# Draw all groups of the contact
|
|
for group in contact.get_shown_groups() + old_grps:
|
|
self.draw_group(group, account)
|
|
self.draw_account(account)
|
|
|
|
return True
|
|
|
|
def rename_self_contact(self, old_jid, new_jid, account):
|
|
"""
|
|
Rename the self_contact jid
|
|
|
|
Keyword arguments:
|
|
old_jid -- our old jid
|
|
new_jid -- our new jid
|
|
account -- the corresponding account.
|
|
"""
|
|
app.contacts.change_contact_jid(old_jid, new_jid, account)
|
|
self_iter = self._get_self_contact_iter(account, model=self.model)
|
|
if not self_iter:
|
|
return
|
|
self.model[self_iter][Column.JID] = new_jid
|
|
self.draw_contact(new_jid, account)
|
|
|
|
def minimize_groupchat(self, account, jid, status=''):
|
|
gc_control = app.interface.msg_win_mgr.get_gc_control(jid, account)
|
|
app.interface.minimized_controls[account][jid] = gc_control
|
|
self.add_groupchat(jid, account)
|
|
|
|
def add_groupchat(self, jid, account):
|
|
"""
|
|
Add groupchat to roster and draw it. Return the added contact instance
|
|
"""
|
|
contact = app.contacts.get_groupchat_contact(account, jid)
|
|
show = 'offline'
|
|
if app.account_is_available(account):
|
|
show = 'online'
|
|
|
|
contact.show = show
|
|
self.add_contact(jid, account)
|
|
|
|
return contact
|
|
|
|
def remove_groupchat(self, jid, account, maximize=False):
|
|
"""
|
|
Remove groupchat from roster and redraw account and group
|
|
"""
|
|
contact = app.contacts.get_contact_with_highest_priority(account, jid)
|
|
if contact.is_groupchat:
|
|
if jid in app.interface.minimized_controls[account]:
|
|
del app.interface.minimized_controls[account][jid]
|
|
self.remove_contact(jid, account, force=True, backend=True, maximize=maximize)
|
|
return True
|
|
return False
|
|
|
|
# FIXME: This function is yet unused! Port to new API
|
|
def add_transport(self, jid, account):
|
|
"""
|
|
Add transport to roster and draw it. Return the added contact instance
|
|
"""
|
|
contact = app.contacts.get_contact_with_highest_priority(account, jid)
|
|
if contact is None:
|
|
contact = app.contacts.create_contact(jid=jid, account=account,
|
|
name=jid, groups=[_('Transports')], show='offline',
|
|
status='offline', sub='from')
|
|
app.contacts.add_contact(account, contact)
|
|
self.add_contact(jid, account)
|
|
return contact
|
|
|
|
def remove_transport(self, jid, account):
|
|
"""
|
|
Remove transport from roster and redraw account and group
|
|
"""
|
|
self.remove_contact(jid, account, force=True, backend=True)
|
|
return True
|
|
|
|
def rename_group(self, old_name, new_name, account):
|
|
"""
|
|
Rename a roster group
|
|
"""
|
|
if old_name == new_name:
|
|
return
|
|
|
|
# Groups may not change name from or to a special groups
|
|
for g in helpers.special_groups:
|
|
if g in (new_name, old_name):
|
|
return
|
|
|
|
# update all contacts in the given group
|
|
if self.regroup:
|
|
accounts = app.connections.keys()
|
|
else:
|
|
accounts = [account, ]
|
|
|
|
for acc in accounts:
|
|
changed_contacts = []
|
|
for jid in app.contacts.get_jid_list(acc):
|
|
contact = app.contacts.get_first_contact_from_jid(acc, jid)
|
|
if old_name not in contact.groups:
|
|
continue
|
|
|
|
self.remove_contact(jid, acc, force=True)
|
|
|
|
contact.groups.remove(old_name)
|
|
if new_name not in contact.groups:
|
|
contact.groups.append(new_name)
|
|
|
|
changed_contacts.append({'jid': jid, 'name': contact.name,
|
|
'groups':contact.groups})
|
|
|
|
app.connections[acc].get_module('Roster').update_contacts(
|
|
changed_contacts)
|
|
|
|
for c in changed_contacts:
|
|
self.add_contact(c['jid'], acc)
|
|
|
|
self._adjust_group_expand_collapse_state(new_name, acc)
|
|
|
|
self.draw_group(old_name, acc)
|
|
self.draw_group(new_name, acc)
|
|
|
|
|
|
def add_contact_to_groups(self, jid, account, groups, update=True):
|
|
"""
|
|
Add contact to given groups and redraw them
|
|
|
|
Contact on server is updated too. When the contact has a family,
|
|
the action will be performed for all members.
|
|
|
|
Keyword Arguments:
|
|
jid -- the jid
|
|
account -- the corresponding account
|
|
groups -- list of Groups to add the contact to.
|
|
update -- update contact on the server
|
|
"""
|
|
self.remove_contact(jid, account, force=True)
|
|
for contact in app.contacts.get_contacts(account, jid):
|
|
for group in groups:
|
|
if group not in contact.groups:
|
|
# we might be dropped from meta to group
|
|
contact.groups.append(group)
|
|
if update:
|
|
con = app.connections[account]
|
|
con.get_module('Roster').update_contact(
|
|
jid, contact.name, contact.groups)
|
|
|
|
self.add_contact(jid, account)
|
|
|
|
for group in groups:
|
|
self._adjust_group_expand_collapse_state(group, account)
|
|
|
|
def remove_contact_from_groups(self, jid, account, groups, update=True):
|
|
"""
|
|
Remove contact from given groups and redraw them
|
|
|
|
Contact on server is updated too. When the contact has a family,
|
|
the action will be performed for all members.
|
|
|
|
Keyword Arguments:
|
|
jid -- the jid
|
|
account -- the corresponding account
|
|
groups -- list of Groups to remove the contact from
|
|
update -- update contact on the server
|
|
"""
|
|
self.remove_contact(jid, account, force=True)
|
|
for contact in app.contacts.get_contacts(account, jid):
|
|
for group in groups:
|
|
if group in contact.groups:
|
|
# Needed when we remove from "General" or "Observers"
|
|
contact.groups.remove(group)
|
|
if update:
|
|
con = app.connections[account]
|
|
con.get_module('Roster').update_contact(
|
|
jid, contact.name, contact.groups)
|
|
self.add_contact(jid, account)
|
|
|
|
# Also redraw old groups
|
|
for group in groups:
|
|
self.draw_group(group, account)
|
|
|
|
# FIXME: maybe move to app.py
|
|
def remove_newly_added(self, jid, account):
|
|
if account not in app.newly_added:
|
|
# Account has been deleted during the timeout that called us
|
|
return
|
|
if jid in app.newly_added[account]:
|
|
app.newly_added[account].remove(jid)
|
|
self.draw_contact(jid, account)
|
|
|
|
# FIXME: maybe move to app.py
|
|
def remove_to_be_removed(self, jid, account):
|
|
if account not in app.interface.instances:
|
|
# Account has been deleted during the timeout that called us
|
|
return
|
|
if jid in app.newly_added[account]:
|
|
return
|
|
if jid in app.to_be_removed[account]:
|
|
app.to_be_removed[account].remove(jid)
|
|
family = app.contacts.get_metacontacts_family(account, jid)
|
|
if family:
|
|
# Perform delayed recalibration
|
|
self._recalibrate_metacontact_family(family, account)
|
|
self.draw_contact(jid, account)
|
|
# Hide Group if all children are hidden
|
|
contact = app.contacts.get_contact(account, jid)
|
|
if not contact:
|
|
return
|
|
for group in contact.get_shown_groups():
|
|
self.draw_group(group, account)
|
|
|
|
# FIXME: integrate into add_contact()
|
|
def add_to_not_in_the_roster(self, account, jid, nick='', resource='',
|
|
groupchat=False):
|
|
contact = app.contacts.create_not_in_roster_contact(
|
|
jid=jid, account=account, resource=resource, name=nick,
|
|
groupchat=groupchat)
|
|
app.contacts.add_contact(account, contact)
|
|
self.add_contact(contact.jid, account)
|
|
return contact
|
|
|
|
|
|
################################################################################
|
|
### Methods for adding and removing roster window items
|
|
################################################################################
|
|
|
|
def _really_draw_account(self, account):
|
|
child_iter = self._get_account_iter(account, self.model)
|
|
if not child_iter:
|
|
return
|
|
|
|
if self.regroup:
|
|
account_name = _('Merged accounts')
|
|
accounts = []
|
|
else:
|
|
account_name = app.get_account_label(account)
|
|
accounts = [account]
|
|
|
|
if account in self.collapsed_rows and \
|
|
self.model.iter_has_child(child_iter):
|
|
account_name = '[%s]' % account_name
|
|
|
|
if (app.account_is_available(account) or (self.regroup and \
|
|
app.get_number_of_connected_accounts())) and app.settings.get(
|
|
'show_contacts_number'):
|
|
nbr_on, nbr_total = app.contacts.get_nb_online_total_contacts(
|
|
accounts=accounts)
|
|
account_name += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total))
|
|
|
|
self.model[child_iter][Column.NAME] = GLib.markup_escape_text(account_name)
|
|
|
|
mood_icon_name = get_account_mood_icon_name(account)
|
|
self.model[child_iter][Column.MOOD_PIXBUF] = mood_icon_name
|
|
|
|
activity_icon_name = get_account_activity_icon_name(account)
|
|
self.model[child_iter][Column.ACTIVITY_PIXBUF] = activity_icon_name
|
|
|
|
tune_icon_name = get_account_tune_icon_name(account)
|
|
self.model[child_iter][Column.TUNE_ICON] = tune_icon_name
|
|
|
|
location_icon_name = get_account_location_icon_name(account)
|
|
self.model[child_iter][Column.LOCATION_ICON] = location_icon_name
|
|
|
|
def _really_draw_accounts(self):
|
|
for acct in self.accounts_to_draw:
|
|
self._really_draw_account(acct)
|
|
self.accounts_to_draw = []
|
|
return False
|
|
|
|
def draw_account(self, account):
|
|
if account in self.accounts_to_draw:
|
|
return
|
|
self.accounts_to_draw.append(account)
|
|
if len(self.accounts_to_draw) == 1:
|
|
GLib.timeout_add(200, self._really_draw_accounts)
|
|
|
|
def _really_draw_group(self, group, account):
|
|
child_iter = self._get_group_iter(group, account, model=self.model)
|
|
if not child_iter:
|
|
# Eg. We redraw groups after we removed a entity
|
|
# and its empty groups
|
|
return
|
|
if self.regroup:
|
|
accounts = []
|
|
else:
|
|
accounts = [account]
|
|
text = GLib.markup_escape_text(group)
|
|
if app.settings.get('show_contacts_number'):
|
|
nbr_on, nbr_total = app.contacts.get_nb_online_total_contacts(
|
|
accounts=accounts, groups=[group])
|
|
text += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total))
|
|
|
|
self.model[child_iter][Column.NAME] = text
|
|
|
|
# Hide group if no more contacts
|
|
iterG = self._get_group_iter(group, account, model=self.modelfilter)
|
|
to_hide = []
|
|
while iterG:
|
|
parent = self.modelfilter.iter_parent(iterG)
|
|
if (not self.modelfilter.iter_has_child(iterG)) or (to_hide \
|
|
and self.modelfilter.iter_n_children(iterG) == 1):
|
|
to_hide.append(iterG)
|
|
if not parent or self.modelfilter[parent][Column.TYPE] != \
|
|
'group':
|
|
iterG = None
|
|
else:
|
|
iterG = parent
|
|
else:
|
|
iterG = None
|
|
for iter_ in to_hide:
|
|
self.modelfilter[iter_][Column.VISIBLE] = False
|
|
|
|
def _really_draw_groups(self):
|
|
for ag in self.groups_to_draw.values():
|
|
acct = ag['account']
|
|
grp = ag['group']
|
|
self._really_draw_group(grp, acct)
|
|
self.groups_to_draw = {}
|
|
return False
|
|
|
|
def draw_group(self, group, account):
|
|
ag = account + group
|
|
if ag in self.groups_to_draw:
|
|
return
|
|
self.groups_to_draw[ag] = {'group': group, 'account': account}
|
|
if len(self.groups_to_draw) == 1:
|
|
GLib.timeout_add(200, self._really_draw_groups)
|
|
|
|
def draw_parent_contact(self, jid, account):
|
|
child_iters = self._get_contact_iter(jid, account, model=self.model)
|
|
if not child_iters:
|
|
return False
|
|
parent_iter = self.model.iter_parent(child_iters[0])
|
|
if self.model[parent_iter][Column.TYPE] != 'contact':
|
|
# parent is not a contact
|
|
return
|
|
parent_jid = self.model[parent_iter][Column.JID]
|
|
parent_account = self.model[parent_iter][Column.ACCOUNT]
|
|
self.draw_contact(parent_jid, parent_account)
|
|
return False
|
|
|
|
def draw_contact(self, jid, account, selected=False, focus=False,
|
|
contact_instances=None, contact=None):
|
|
"""
|
|
Draw the correct state image, name BUT not avatar
|
|
"""
|
|
# focus is about if the roster window has toplevel-focus or not
|
|
# FIXME: We really need a custom cell_renderer
|
|
|
|
if not contact_instances:
|
|
contact_instances = app.contacts.get_contacts(account, jid)
|
|
if not contact:
|
|
contact = app.contacts.get_highest_prio_contact_from_contacts(
|
|
contact_instances)
|
|
if not contact:
|
|
return False
|
|
|
|
child_iters = self._get_contact_iter(jid, account, contact, self.model)
|
|
if not child_iters:
|
|
return False
|
|
|
|
name = GLib.markup_escape_text(contact.get_shown_name())
|
|
|
|
# gets number of unread gc marked messages
|
|
if jid in app.interface.minimized_controls[account] and \
|
|
app.interface.minimized_controls[account][jid]:
|
|
nb_unread = len(app.events.get_events(account, jid,
|
|
['printed_marked_gc_msg']))
|
|
nb_unread += app.interface.minimized_controls \
|
|
[account][jid].get_nb_unread_pm()
|
|
|
|
if nb_unread == 1:
|
|
name = '%s *' % name
|
|
elif nb_unread > 1:
|
|
name = '%s [%s]' % (name, str(nb_unread))
|
|
|
|
# Strike name if blocked
|
|
strike = helpers.jid_is_blocked(account, jid)
|
|
if strike:
|
|
name = '<span strikethrough="true">%s</span>' % name
|
|
|
|
# Show resource counter
|
|
nb_connected_contact = 0
|
|
for c in contact_instances:
|
|
if c.show not in ('error', 'offline'):
|
|
nb_connected_contact += 1
|
|
if nb_connected_contact > 1:
|
|
# switch back to default writing direction
|
|
name += i18n.paragraph_direction_mark(name)
|
|
name += ' (%d)' % nb_connected_contact
|
|
|
|
# add status msg, if not empty, under contact name in
|
|
# the treeview
|
|
if app.settings.get('show_status_msgs_in_roster'):
|
|
status_span = '\n<span size="small" style="italic" ' \
|
|
'alpha="70%">{}</span>'
|
|
if contact.is_groupchat:
|
|
disco_info = app.storage.cache.get_last_disco_info(contact.jid)
|
|
if disco_info is not None:
|
|
description = disco_info.muc_description
|
|
if description:
|
|
name += status_span.format(
|
|
GLib.markup_escape_text(description))
|
|
elif contact.status:
|
|
status = contact.status.strip()
|
|
if status != '':
|
|
status = helpers.reduce_chars_newlines(
|
|
status, max_lines=1)
|
|
name += status_span.format(
|
|
GLib.markup_escape_text(status))
|
|
|
|
icon_name = helpers.get_icon_name_to_show(contact, account)
|
|
# look if another resource has awaiting events
|
|
for c in contact_instances:
|
|
c_icon_name = helpers.get_icon_name_to_show(c, account)
|
|
if c_icon_name in ('event', 'muc-active', 'muc-inactive'):
|
|
icon_name = c_icon_name
|
|
break
|
|
|
|
# Check for events of collapsed (hidden) brothers
|
|
family = app.contacts.get_metacontacts_family(account, jid)
|
|
is_big_brother = False
|
|
have_visible_children = False
|
|
if family:
|
|
bb_jid, bb_account = \
|
|
self._get_nearby_family_and_big_brother(family, account)[1:]
|
|
is_big_brother = (jid, account) == (bb_jid, bb_account)
|
|
iters = self._get_contact_iter(jid, account)
|
|
have_visible_children = iters and \
|
|
self.modelfilter.iter_has_child(iters[0])
|
|
|
|
if have_visible_children:
|
|
# We are the big brother and have a visible family
|
|
for child_iter in child_iters:
|
|
child_path = self.model.get_path(child_iter)
|
|
path = self.modelfilter.convert_child_path_to_path(child_path)
|
|
|
|
if not path:
|
|
continue
|
|
|
|
if not self.tree.row_expanded(path) and icon_name != 'event':
|
|
iterC = self.model.iter_children(child_iter)
|
|
while iterC:
|
|
# a child has awaiting messages?
|
|
jidC = self.model[iterC][Column.JID]
|
|
accountC = self.model[iterC][Column.ACCOUNT]
|
|
if app.events.get_events(accountC, jidC):
|
|
icon_name = 'event'
|
|
break
|
|
iterC = self.model.iter_next(iterC)
|
|
|
|
if self.tree.row_expanded(path):
|
|
icon_name += ':opened'
|
|
else:
|
|
icon_name += ':closed'
|
|
|
|
theme_icon = get_icon_name(icon_name)
|
|
self.model[child_iter][Column.IMG] = theme_icon
|
|
self.model[child_iter][Column.NAME] = name
|
|
#TODO: compute visible
|
|
visible = True
|
|
self.model[child_iter][Column.VISIBLE] = visible
|
|
else:
|
|
# A normal contact or little brother
|
|
transport = app.get_transport_name_from_jid(jid)
|
|
if transport == 'jabber':
|
|
transport = None
|
|
theme_icon = get_icon_name(icon_name, transport=transport)
|
|
|
|
visible = self.contact_is_visible(contact, account)
|
|
# All iters have the same icon (no expand/collapse)
|
|
for child_iter in child_iters:
|
|
self.model[child_iter][Column.IMG] = theme_icon
|
|
self.model[child_iter][Column.NAME] = name
|
|
self.model[child_iter][Column.VISIBLE] = visible
|
|
if visible:
|
|
parent_iter = self.model.iter_parent(child_iter)
|
|
self.model[parent_iter][Column.VISIBLE] = True
|
|
|
|
# We are a little brother
|
|
if family and not is_big_brother and not self.starting:
|
|
self.draw_parent_contact(jid, account)
|
|
|
|
if visible:
|
|
delimiter = app.connections[account].get_module('Delimiter').delimiter
|
|
for group in contact.get_shown_groups():
|
|
group_splited = group.split(delimiter)
|
|
i = 1
|
|
while i < len(group_splited) + 1:
|
|
g = delimiter.join(group_splited[:i])
|
|
iterG = self._get_group_iter(g, account, model=self.model)
|
|
if iterG:
|
|
# it's not self contact
|
|
self.model[iterG][Column.VISIBLE] = True
|
|
i += 1
|
|
|
|
app.plugin_manager.gui_extension_point('roster_draw_contact', self,
|
|
jid, account, contact)
|
|
|
|
return False
|
|
|
|
def _is_pep_shown_in_roster(self, pep_type):
|
|
if pep_type == PEPEventType.MOOD:
|
|
return app.settings.get('show_mood_in_roster')
|
|
|
|
if pep_type == PEPEventType.ACTIVITY:
|
|
return app.settings.get('show_activity_in_roster')
|
|
|
|
if pep_type == PEPEventType.TUNE:
|
|
return app.settings.get('show_tunes_in_roster')
|
|
|
|
if pep_type == PEPEventType.LOCATION:
|
|
return app.settings.get('show_location_in_roster')
|
|
|
|
return False
|
|
|
|
def draw_all_pep_types(self, jid, account, contact=None):
|
|
self._draw_pep(account, jid, PEPEventType.MOOD)
|
|
self._draw_pep(account, jid, PEPEventType.ACTIVITY)
|
|
self._draw_pep(account, jid, PEPEventType.TUNE)
|
|
self._draw_pep(account, jid, PEPEventType.LOCATION)
|
|
|
|
def _draw_pep(self, account, jid, type_):
|
|
if not self._is_pep_shown_in_roster(type_):
|
|
return
|
|
|
|
iters = self._get_contact_iter(jid, account, model=self.model)
|
|
if not iters:
|
|
return
|
|
contact = app.contacts.get_contact(account, jid)
|
|
|
|
icon = None
|
|
data = contact.pep.get(type_)
|
|
|
|
if type_ == PEPEventType.MOOD:
|
|
column = Column.MOOD_PIXBUF
|
|
if data is not None:
|
|
icon = 'mood-%s' % data.mood
|
|
elif type_ == PEPEventType.ACTIVITY:
|
|
column = Column.ACTIVITY_PIXBUF
|
|
if data is not None:
|
|
icon = get_activity_icon_name(data.activity, data.subactivity)
|
|
elif type_ == PEPEventType.TUNE:
|
|
column = Column.TUNE_ICON
|
|
if data is not None:
|
|
icon = 'audio-x-generic'
|
|
elif type_ == PEPEventType.LOCATION:
|
|
column = Column.LOCATION_ICON
|
|
if data is not None:
|
|
icon = 'applications-internet'
|
|
|
|
for child_iter in iters:
|
|
self.model[child_iter][column] = icon
|
|
|
|
def _get_avatar_image(self, account, jid):
|
|
if not app.settings.get('show_avatars_in_roster'):
|
|
return None
|
|
scale = self.window.get_scale_factor()
|
|
surface = app.contacts.get_avatar(
|
|
account, jid, AvatarSize.ROSTER, scale)
|
|
return Gtk.Image.new_from_surface(surface)
|
|
|
|
def draw_avatar(self, jid, account):
|
|
iters = self._get_contact_iter(jid, account, model=self.model)
|
|
if not iters or not app.settings.get('show_avatars_in_roster'):
|
|
return
|
|
jid = self.model[iters[0]][Column.JID]
|
|
image = self._get_avatar_image(account, jid)
|
|
|
|
for child_iter in iters:
|
|
self.model[child_iter][Column.AVATAR_IMG] = image
|
|
return False
|
|
|
|
def draw_completely(self, jid, account):
|
|
contact_instances = app.contacts.get_contacts(account, jid)
|
|
contact = app.contacts.get_highest_prio_contact_from_contacts(
|
|
contact_instances)
|
|
self.draw_contact(
|
|
jid, account,
|
|
contact_instances=contact_instances,
|
|
contact=contact)
|
|
|
|
def adjust_and_draw_contact_context(self, jid, account):
|
|
"""
|
|
Draw contact, account and groups of given jid Show contact if it has
|
|
pending events
|
|
"""
|
|
contact = app.contacts.get_first_contact_from_jid(account, jid)
|
|
if not contact:
|
|
# idle draw or just removed SelfContact
|
|
return
|
|
|
|
family = app.contacts.get_metacontacts_family(account, jid)
|
|
if family:
|
|
# There might be a new big brother
|
|
self._recalibrate_metacontact_family(family, account)
|
|
self.draw_contact(jid, account)
|
|
self.draw_account(account)
|
|
|
|
for group in contact.get_shown_groups():
|
|
self.draw_group(group, account)
|
|
self._adjust_group_expand_collapse_state(group, account)
|
|
|
|
def _idle_draw_jids_of_account(self, jids, account):
|
|
"""
|
|
Draw given contacts and their avatars in a lazy fashion
|
|
|
|
Keyword arguments:
|
|
jids -- a list of jids to draw
|
|
account -- the corresponding account
|
|
"""
|
|
def _draw_all_contacts(jids, account):
|
|
for jid in jids:
|
|
family = app.contacts.get_metacontacts_family(account, jid)
|
|
if family:
|
|
# For metacontacts over several accounts:
|
|
# When we connect a new account existing brothers
|
|
# must be redrawn (got removed and added again)
|
|
for data in family:
|
|
self.draw_completely(data['jid'], data['account'])
|
|
else:
|
|
self.draw_completely(jid, account)
|
|
yield True
|
|
self.refilter_shown_roster_items()
|
|
yield False
|
|
|
|
task = _draw_all_contacts(jids, account)
|
|
GLib.idle_add(next, task)
|
|
|
|
def _before_fill(self):
|
|
self.tree.freeze_child_notify()
|
|
self.tree.set_model(None)
|
|
# disable sorting
|
|
self.model.set_sort_column_id(-2, Gtk.SortType.ASCENDING)
|
|
self.starting = True
|
|
self.starting_filtering = True
|
|
|
|
def _after_fill(self):
|
|
self.starting = False
|
|
accounts_list = app.contacts.get_accounts()
|
|
for account in app.connections:
|
|
if account not in accounts_list:
|
|
continue
|
|
|
|
jids = app.contacts.get_jid_list(account)
|
|
for jid in jids:
|
|
self.draw_completely(jid, account)
|
|
|
|
# Draw all known groups
|
|
for group in app.groups[account]:
|
|
self.draw_group(group, account)
|
|
self.draw_account(account)
|
|
|
|
self.model.set_sort_column_id(1, Gtk.SortType.ASCENDING)
|
|
self.tree.set_model(self.modelfilter)
|
|
self.tree.thaw_child_notify()
|
|
self.starting_filtering = False
|
|
self.refilter_shown_roster_items()
|
|
|
|
def setup_and_draw_roster(self):
|
|
"""
|
|
Create new empty model and draw roster
|
|
"""
|
|
self.modelfilter = None
|
|
self.model = Gtk.TreeStore(*self.columns)
|
|
|
|
self.model.set_sort_func(1, self._compareIters)
|
|
self.model.set_sort_column_id(1, Gtk.SortType.ASCENDING)
|
|
self.modelfilter = self.model.filter_new()
|
|
self.modelfilter.set_visible_func(self._visible_func)
|
|
self.modelfilter.connect('row-has-child-toggled',
|
|
self.on_modelfilter_row_has_child_toggled)
|
|
self.tree.set_model(self.modelfilter)
|
|
|
|
self._iters = {}
|
|
# for merged mode
|
|
self._iters['MERGED'] = {'account': None, 'groups': {}}
|
|
for acct in app.contacts.get_accounts():
|
|
self._iters[acct] = {'account': None, 'groups': {}, 'contacts': {}}
|
|
|
|
for acct in app.contacts.get_accounts():
|
|
self.add_account(acct)
|
|
self.add_account_contacts(acct, improve_speed=True,
|
|
draw_contacts=False)
|
|
|
|
# Recalculate column width for ellipsizing
|
|
self.tree.columns_autosize()
|
|
|
|
def update_status_selector(self):
|
|
self._status_selector.update()
|
|
|
|
def select_contact(self, jid, account):
|
|
"""
|
|
Select contact in roster. If contact is hidden but has events, show him
|
|
"""
|
|
# Refiltering SHOULD NOT be needed:
|
|
# When a contact gets a new event he will be redrawn and his
|
|
# icon changes, so _visible_func WILL be called on him anyway
|
|
iters = self._get_contact_iter(jid, account)
|
|
if not iters:
|
|
# Not visible in roster
|
|
return
|
|
path = self.modelfilter.get_path(iters[0])
|
|
if self.dragging or not app.settings.get(
|
|
'scroll_roster_to_last_message'):
|
|
# do not change selection while DND'ing
|
|
return
|
|
# Expand his parent, so this path is visible, don't expand it.
|
|
path.up()
|
|
self.tree.expand_to_path(path)
|
|
self.tree.scroll_to_cell(path)
|
|
self.tree.set_cursor(path)
|
|
|
|
def _readjust_expand_collapse_state(self):
|
|
def func(model, path, iter_, param):
|
|
type_ = model[iter_][Column.TYPE]
|
|
acct = model[iter_][Column.ACCOUNT]
|
|
jid = model[iter_][Column.JID]
|
|
key = None
|
|
if type_ == 'account':
|
|
key = acct
|
|
elif type_ == 'group':
|
|
key = acct + jid
|
|
elif type_ == 'contact':
|
|
parent_iter = model.iter_parent(iter_)
|
|
ptype = model[parent_iter][Column.TYPE]
|
|
if ptype == 'group':
|
|
grp = model[parent_iter][Column.JID]
|
|
key = acct + grp + jid
|
|
if key:
|
|
if key in self.collapsed_rows:
|
|
self.tree.collapse_row(path)
|
|
else:
|
|
self.tree.expand_row(path, False)
|
|
self.modelfilter.foreach(func, None)
|
|
|
|
def _adjust_account_expand_collapse_state(self, account):
|
|
"""
|
|
Expand/collapse account row based on self.collapsed_rows
|
|
"""
|
|
if not self.tree.get_model():
|
|
return
|
|
iterA = self._get_account_iter(account)
|
|
if not iterA:
|
|
# thank you modelfilter
|
|
return
|
|
path = self.modelfilter.get_path(iterA)
|
|
if account in self.collapsed_rows:
|
|
self.tree.collapse_row(path)
|
|
else:
|
|
self.tree.expand_row(path, False)
|
|
return False
|
|
|
|
|
|
def _adjust_group_expand_collapse_state(self, group, account):
|
|
"""
|
|
Expand/collapse group row based on self.collapsed_rows
|
|
"""
|
|
if not self.tree.get_model():
|
|
return
|
|
if account not in app.connections:
|
|
return
|
|
delimiter = app.connections[account].get_module('Delimiter').delimiter
|
|
group_splited = group.split(delimiter)
|
|
i = 1
|
|
while i < len(group_splited) + 1:
|
|
g = delimiter.join(group_splited[:i])
|
|
iterG = self._get_group_iter(g, account)
|
|
if not iterG:
|
|
# Group not visible
|
|
return
|
|
path = self.modelfilter.get_path(iterG)
|
|
if account + g in self.collapsed_rows:
|
|
self.tree.collapse_row(path)
|
|
else:
|
|
self.tree.expand_row(path, False)
|
|
i += 1
|
|
|
|
##############################################################################
|
|
### Roster and Modelfilter handling
|
|
##############################################################################
|
|
|
|
def refilter_shown_roster_items(self):
|
|
if self.filtering:
|
|
return
|
|
self.filtering = True
|
|
for account in app.connections:
|
|
for jid in app.contacts.get_jid_list(account):
|
|
self.adjust_and_draw_contact_context(jid, account)
|
|
self.filtering = False
|
|
|
|
def contact_has_pending_roster_events(self, contact, account):
|
|
"""
|
|
Return True if the contact or one if it resources has pending events
|
|
"""
|
|
# jid has pending events
|
|
if app.events.get_nb_roster_events(account, contact.jid) > 0:
|
|
return True
|
|
# check events of all resources
|
|
for contact_ in app.contacts.get_contacts(account, contact.jid):
|
|
if contact_.resource and app.events.get_nb_roster_events(account,
|
|
contact_.get_full_jid()) > 0:
|
|
return True
|
|
return False
|
|
|
|
def contact_is_visible(self, contact, account):
|
|
if self.rfilter_enabled:
|
|
return self.rfilter_string in contact.get_shown_name().lower()
|
|
if self.contact_has_pending_roster_events(contact, account):
|
|
return True
|
|
if app.settings.get('showoffline'):
|
|
return True
|
|
|
|
if contact.show in ('offline', 'error'):
|
|
if contact.jid in app.to_be_removed[account]:
|
|
return True
|
|
return False
|
|
if app.settings.get('show_only_chat_and_online') and contact.show in (
|
|
'away', 'xa', 'busy'):
|
|
return False
|
|
if _('Transports') in contact.get_shown_groups():
|
|
return app.settings.get('show_transports_group')
|
|
return True
|
|
|
|
def _visible_func(self, model, titer, dummy):
|
|
"""
|
|
Determine whether iter should be visible in the treeview
|
|
"""
|
|
if self.starting_filtering:
|
|
return False
|
|
|
|
visible = model[titer][Column.VISIBLE]
|
|
|
|
type_ = model[titer][Column.TYPE]
|
|
if not type_:
|
|
return False
|
|
if type_ == 'account':
|
|
# Always show account
|
|
return True
|
|
|
|
account = model[titer][Column.ACCOUNT]
|
|
if not account:
|
|
return False
|
|
|
|
jid = model[titer][Column.JID]
|
|
if not jid:
|
|
return False
|
|
|
|
if not self.rfilter_enabled:
|
|
return visible
|
|
|
|
if type_ == 'group':
|
|
group = jid
|
|
if group == _('Transports'):
|
|
if self.regroup:
|
|
accounts = app.contacts.get_accounts()
|
|
else:
|
|
accounts = [account]
|
|
for _acc in accounts:
|
|
for contact in app.contacts.iter_contacts(_acc):
|
|
if group in contact.get_shown_groups():
|
|
if self.rfilter_string in \
|
|
contact.get_shown_name().lower():
|
|
return True
|
|
elif self.contact_has_pending_roster_events(contact,
|
|
_acc):
|
|
return True
|
|
# No transport has been found
|
|
return False
|
|
|
|
if type_ == 'contact':
|
|
if model.iter_has_child(titer):
|
|
iter_c = model.iter_children(titer)
|
|
while iter_c:
|
|
if self.rfilter_string in model[iter_c][Column.NAME].lower():
|
|
return True
|
|
iter_c = model.iter_next(iter_c)
|
|
return self.rfilter_string in model[titer][Column.NAME].lower()
|
|
|
|
if type_ == 'agent':
|
|
return self.rfilter_string in model[titer][Column.NAME].lower()
|
|
|
|
if type_ == 'groupchat':
|
|
return self.rfilter_string in model[titer][Column.NAME].lower()
|
|
|
|
return visible
|
|
|
|
def _compareIters(self, model, iter1, iter2, data=None):
|
|
"""
|
|
Compare two iters to sort them
|
|
"""
|
|
name1 = model[iter1][Column.NAME]
|
|
name2 = model[iter2][Column.NAME]
|
|
if not name1 or not name2:
|
|
return 0
|
|
type1 = model[iter1][Column.TYPE]
|
|
type2 = model[iter2][Column.TYPE]
|
|
if type1 == 'self_contact':
|
|
return -1
|
|
if type2 == 'self_contact':
|
|
return 1
|
|
if type1 == 'group':
|
|
name1 = model[iter1][Column.JID]
|
|
name2 = model[iter2][Column.JID]
|
|
if name1 == _('Transports'):
|
|
return 1
|
|
if name2 == _('Transports'):
|
|
return -1
|
|
if name1 == _('Not in contact list'):
|
|
return 1
|
|
if name2 == _('Not in contact list'):
|
|
return -1
|
|
if name1 == _('Group chats'):
|
|
return 1
|
|
if name2 == _('Group chats'):
|
|
return -1
|
|
account1 = model[iter1][Column.ACCOUNT]
|
|
account2 = model[iter2][Column.ACCOUNT]
|
|
if not account1 or not account2:
|
|
return 0
|
|
if type1 == 'account':
|
|
return locale.strcoll(account1, account2)
|
|
jid1 = model[iter1][Column.JID]
|
|
jid2 = model[iter2][Column.JID]
|
|
if type1 == 'contact':
|
|
lcontact1 = app.contacts.get_contacts(account1, jid1)
|
|
contact1 = app.contacts.get_first_contact_from_jid(account1, jid1)
|
|
if not contact1:
|
|
return 0
|
|
name1 = contact1.get_shown_name()
|
|
if type2 == 'contact':
|
|
lcontact2 = app.contacts.get_contacts(account2, jid2)
|
|
contact2 = app.contacts.get_first_contact_from_jid(account2, jid2)
|
|
if not contact2:
|
|
return 0
|
|
name2 = contact2.get_shown_name()
|
|
# We first compare by show if sort_by_show_in_roster is True or if it's
|
|
# a child contact
|
|
if type1 == 'contact' and type2 == 'contact' and \
|
|
app.settings.get('sort_by_show_in_roster'):
|
|
cshow = {'chat':0, 'online': 1, 'away': 2, 'xa': 3, 'dnd': 4,
|
|
'offline': 6, 'not in roster': 7, 'error': 8}
|
|
s = self.get_show(lcontact1)
|
|
show1 = cshow.get(s, 9)
|
|
s = self.get_show(lcontact2)
|
|
show2 = cshow.get(s, 9)
|
|
removing1 = False
|
|
removing2 = False
|
|
if show1 == 6 and jid1 in app.to_be_removed[account1]:
|
|
removing1 = True
|
|
if show2 == 6 and jid2 in app.to_be_removed[account2]:
|
|
removing2 = True
|
|
if removing1 and not removing2:
|
|
return 1
|
|
if removing2 and not removing1:
|
|
return -1
|
|
sub1 = contact1.sub
|
|
sub2 = contact2.sub
|
|
# none and from goes after
|
|
if sub1 not in ['none', 'from'] and sub2 in ['none', 'from']:
|
|
return -1
|
|
if sub1 in ['none', 'from'] and sub2 not in ['none', 'from']:
|
|
return 1
|
|
if show1 < show2:
|
|
return -1
|
|
if show1 > show2:
|
|
return 1
|
|
# We compare names
|
|
cmp_result = locale.strcoll(name1.lower(), name2.lower())
|
|
if cmp_result < 0:
|
|
return -1
|
|
if cmp_result > 0:
|
|
return 1
|
|
if type1 == 'contact' and type2 == 'contact':
|
|
# We compare account names
|
|
cmp_result = locale.strcoll(account1.lower(), account2.lower())
|
|
if cmp_result < 0:
|
|
return -1
|
|
if cmp_result > 0:
|
|
return 1
|
|
# We compare jids
|
|
cmp_result = locale.strcoll(jid1.lower(), jid2.lower())
|
|
if cmp_result < 0:
|
|
return -1
|
|
if cmp_result > 0:
|
|
return 1
|
|
return 0
|
|
|
|
################################################################################
|
|
### FIXME: Methods that don't belong to roster window...
|
|
### ... at least not in there current form
|
|
################################################################################
|
|
|
|
def fire_up_unread_messages_events(self, account):
|
|
"""
|
|
Read from db the unread messages, and fire them up, and if we find very
|
|
old unread messages, delete them from unread table
|
|
"""
|
|
results = app.storage.archive.get_unread_msgs()
|
|
for result, shown in results:
|
|
jid = result.jid
|
|
additional_data = result.additional_data
|
|
if app.contacts.get_first_contact_from_jid(account, jid) and not \
|
|
shown:
|
|
# We have this jid in our contacts list
|
|
# XXX unread messages should probably have their session saved
|
|
# with them
|
|
session = app.connections[account].make_new_session(jid)
|
|
|
|
tim = float(result.time)
|
|
session.roster_message(jid, result.message, tim, msg_type='chat',
|
|
msg_log_id=result.log_line_id, additional_data=additional_data)
|
|
app.storage.archive.set_shown_unread_msgs(result.log_line_id)
|
|
|
|
elif (time.time() - result.time) > 2592000:
|
|
# ok, here we see that we have a message in unread messages
|
|
# table that is older than a month. It is probably from someone
|
|
# not in our roster for accounts we usually launch, so we will
|
|
# delete this id from unread message tables.
|
|
app.storage.archive.set_read_messages([result.log_line_id])
|
|
|
|
def fill_contacts_and_groups_dicts(self, array, account):
|
|
"""
|
|
Fill app.contacts and app.groups
|
|
"""
|
|
# FIXME: This function needs to be split
|
|
# Most of the logic SHOULD NOT be done at GUI level
|
|
if account not in app.contacts.get_accounts():
|
|
app.contacts.add_account(account)
|
|
if not account in self._iters:
|
|
self._iters[account] = {'account': None, 'groups': {},
|
|
'contacts': {}}
|
|
if account not in app.groups:
|
|
app.groups[account] = {}
|
|
|
|
self_jid = str(app.connections[account].get_own_jid())
|
|
if account != app.ZEROCONF_ACC_NAME:
|
|
array[self_jid] = {'name': app.nicks[account],
|
|
'groups': ['self_contact'],
|
|
'subscription': 'both',
|
|
'ask': 'none'}
|
|
|
|
# .keys() is needed
|
|
for jid in list(array.keys()):
|
|
# Remove the contact in roster. It might has changed
|
|
self.remove_contact(jid, account, force=True)
|
|
# Remove old Contact instances
|
|
app.contacts.remove_jid(account, jid, remove_meta=False)
|
|
jids = jid.split('/')
|
|
# get jid
|
|
ji = jids[0]
|
|
# get resource
|
|
resource = ''
|
|
if len(jids) > 1:
|
|
resource = '/'.join(jids[1:])
|
|
# get name
|
|
name = array[jid]['name'] or ''
|
|
show = 'offline' # show is offline by default
|
|
status = '' # no status message by default
|
|
|
|
if app.jid_is_transport(jid):
|
|
array[jid]['groups'] = [_('Transports')]
|
|
#TRANSP - potential
|
|
contact1 = app.contacts.create_contact(jid=ji, account=account,
|
|
name=name, groups=array[jid]['groups'], show=show,
|
|
status=status, sub=array[jid]['subscription'],
|
|
ask=array[jid]['ask'], resource=resource)
|
|
app.contacts.add_contact(account, contact1)
|
|
|
|
# If we already have chat windows opened, update them with new
|
|
# contact instance
|
|
chat_control = app.interface.msg_win_mgr.get_control(ji, account)
|
|
if chat_control:
|
|
chat_control.contact = contact1
|
|
|
|
def connected_rooms(self, account):
|
|
if account in list(app.gc_connected[account].values()):
|
|
return True
|
|
return False
|
|
|
|
def on_event_removed(self, event_list):
|
|
"""
|
|
Remove contacts on last events removed
|
|
|
|
Only performed if removal was requested before but the contact still had
|
|
pending events
|
|
"""
|
|
|
|
msg_log_ids = []
|
|
for ev in event_list:
|
|
if ev.type_ != 'printed_chat':
|
|
continue
|
|
if ev.msg_log_id:
|
|
# There is a msg_log_id
|
|
msg_log_ids.append(ev.msg_log_id)
|
|
|
|
if msg_log_ids:
|
|
app.storage.archive.set_read_messages(msg_log_ids)
|
|
|
|
contact_list = ((event.jid.split('/')[0], event.account) for event in \
|
|
event_list)
|
|
|
|
for jid, account in contact_list:
|
|
self.draw_contact(jid, account)
|
|
# Remove contacts in roster if removal was requested
|
|
key = (jid, account)
|
|
if key in list(self.contacts_to_be_removed.keys()):
|
|
backend = self.contacts_to_be_removed[key]['backend']
|
|
del self.contacts_to_be_removed[key]
|
|
# Remove contact will delay removal if there are more events
|
|
# pending
|
|
self.remove_contact(jid, account, backend=backend)
|
|
self.show_title()
|
|
|
|
def open_event(self, account, jid, event):
|
|
"""
|
|
If an event was handled, return True, else return False
|
|
"""
|
|
ft = app.interface.instances['file_transfers']
|
|
event = app.events.get_first_event(account, jid, event.type_)
|
|
if event.type_ == 'normal':
|
|
SingleMessageWindow(account, jid,
|
|
action='receive', from_whom=jid, subject=event.subject,
|
|
message=event.message, resource=event.resource)
|
|
app.events.remove_events(account, jid, event)
|
|
return True
|
|
|
|
if event.type_ == 'file-request':
|
|
contact = app.contacts.get_contact_with_highest_priority(account,
|
|
jid)
|
|
ft.show_file_request(account, contact, event.file_props)
|
|
app.events.remove_events(account, jid, event)
|
|
return True
|
|
|
|
if event.type_ in ('file-request-error', 'file-send-error'):
|
|
ft.show_send_error(event.file_props)
|
|
app.events.remove_events(account, jid, event)
|
|
return True
|
|
|
|
if event.type_ in ('file-error', 'file-stopped'):
|
|
msg_err = ''
|
|
if event.file_props.error == -1:
|
|
msg_err = _('Remote contact stopped transfer')
|
|
elif event.file_props.error == -6:
|
|
msg_err = _('Error opening file')
|
|
ft.show_stopped(jid, event.file_props, error_msg=msg_err)
|
|
app.events.remove_events(account, jid, event)
|
|
return True
|
|
|
|
if event.type_ == 'file-hash-error':
|
|
ft.show_hash_error(jid, event.file_props, account)
|
|
app.events.remove_events(account, jid, event)
|
|
return True
|
|
|
|
if event.type_ == 'file-completed':
|
|
ft.show_completed(jid, event.file_props)
|
|
app.events.remove_events(account, jid, event)
|
|
return True
|
|
|
|
if event.type_ == 'gc-invitation':
|
|
open_window('GroupChatInvitation',
|
|
account=account,
|
|
event=event)
|
|
app.events.remove_events(account, jid, event)
|
|
return True
|
|
|
|
if event.type_ == 'subscription_request':
|
|
open_window('SubscriptionRequest',
|
|
account=account,
|
|
jid=jid,
|
|
text=event.text,
|
|
user_nick=event.nick)
|
|
app.events.remove_events(account, jid, event)
|
|
return True
|
|
|
|
if event.type_ == 'unsubscribed':
|
|
app.interface.show_unsubscribed_dialog(account, event.contact)
|
|
app.events.remove_events(account, jid, event)
|
|
return True
|
|
|
|
if event.type_ == 'jingle-incoming':
|
|
ctrl = app.interface.msg_win_mgr.get_control(jid, account)
|
|
if ctrl:
|
|
ctrl.parent_win.set_active_tab(ctrl)
|
|
else:
|
|
ctrl = app.interface.new_chat_from_jid(account, jid)
|
|
ctrl.add_call_received_message(event)
|
|
return True
|
|
|
|
return False
|
|
|
|
################################################################################
|
|
### This and that... random.
|
|
################################################################################
|
|
|
|
def show_roster_vbox(self, active):
|
|
vb = self.xml.get_object('roster_vbox2')
|
|
if active:
|
|
vb.set_no_show_all(False)
|
|
vb.show()
|
|
else:
|
|
vb.hide()
|
|
vb.set_no_show_all(True)
|
|
|
|
def authorize(self, widget, jid, account):
|
|
"""
|
|
Authorize a contact (by re-sending auth menuitem)
|
|
"""
|
|
app.connections[account].get_module('Presence').subscribed(jid)
|
|
InformationDialog(_('Authorization sent'),
|
|
_('"%s" will now see your status.') %jid)
|
|
|
|
def req_sub(self, widget, jid, txt, account, groups=None, nickname=None,
|
|
auto_auth=False):
|
|
"""
|
|
Request subscription to a contact
|
|
"""
|
|
groups_list = groups or []
|
|
app.connections[account].get_module('Presence').subscribe(
|
|
jid, txt, nickname, groups_list, auto_auth)
|
|
contact = app.contacts.get_contact_with_highest_priority(account, jid)
|
|
if not contact:
|
|
contact = app.contacts.create_contact(jid=jid, account=account,
|
|
name=nickname, groups=groups_list, show='requested', status='',
|
|
ask='none', sub='subscribe')
|
|
app.contacts.add_contact(account, contact)
|
|
else:
|
|
if not _('Not in contact list') in contact.get_shown_groups():
|
|
InformationDialog(_('Subscription request has been '
|
|
'sent'), _('If "%s" accepts this request you will know '
|
|
'their status.') % jid)
|
|
return
|
|
self.remove_contact(contact.jid, account, force=True)
|
|
contact.groups = groups_list
|
|
if nickname:
|
|
contact.name = nickname
|
|
self.add_contact(jid, account)
|
|
|
|
def revoke_auth(self, widget, jid, account):
|
|
"""
|
|
Revoke a contact's authorization
|
|
"""
|
|
app.connections[account].get_module('Presence').unsubscribed(jid)
|
|
InformationDialog(_('Authorization removed'),
|
|
_('Now "%s" will always see you as offline.') %jid)
|
|
|
|
def set_state(self, account, state):
|
|
child_iterA = self._get_account_iter(account, self.model)
|
|
if child_iterA:
|
|
self.model[child_iterA][0] = get_icon_name(state)
|
|
if app.interface.systray_enabled:
|
|
app.interface.systray.change_status(state)
|
|
|
|
def set_connecting_state(self, account):
|
|
self.set_state(account, 'connecting')
|
|
|
|
def send_status(self, account, status, txt):
|
|
if status != 'offline':
|
|
app.settings.set_account_setting(account, 'last_status', status)
|
|
app.settings.set_account_setting(account, 'last_status_msg',
|
|
helpers.to_one_line(txt))
|
|
if not app.account_is_available(account):
|
|
self.set_connecting_state(account)
|
|
|
|
if status == 'offline':
|
|
self.delete_pep(app.get_jid_from_account(account), account)
|
|
|
|
app.connections[account].change_status(status, txt)
|
|
self._status_selector.update()
|
|
|
|
def delete_pep(self, jid, account):
|
|
if jid == app.get_jid_from_account(account):
|
|
app.connections[account].pep = {}
|
|
self.draw_account(account)
|
|
|
|
for contact in app.contacts.get_contacts(account, jid):
|
|
contact.pep = {}
|
|
|
|
self.draw_all_pep_types(jid, account)
|
|
ctrl = app.interface.msg_win_mgr.get_control(jid, account)
|
|
if ctrl:
|
|
ctrl.update_all_pep_types()
|
|
|
|
def chg_contact_status(self, contact, show, status_message, account):
|
|
"""
|
|
When a contact changes their status
|
|
"""
|
|
contact_instances = app.contacts.get_contacts(account, contact.jid)
|
|
contact.show = show
|
|
contact.status = status_message
|
|
# name is to show in conversation window
|
|
name = contact.get_shown_name()
|
|
fjid = contact.get_full_jid()
|
|
|
|
# The contact has several resources
|
|
if len(contact_instances) > 1:
|
|
if contact.resource:
|
|
name += '/' + contact.resource
|
|
|
|
# Remove resource when going offline
|
|
if show in ('offline', 'error') and \
|
|
not self.contact_has_pending_roster_events(contact, account):
|
|
ctrl = app.interface.msg_win_mgr.get_control(fjid, account)
|
|
if ctrl:
|
|
ctrl.update_ui()
|
|
ctrl.parent_win.redraw_tab(ctrl)
|
|
# keep the contact around, since it's
|
|
# already attached to the control
|
|
else:
|
|
app.contacts.remove_contact(account, contact)
|
|
|
|
elif contact.jid == app.get_jid_from_account(account) and \
|
|
show in ('offline', 'error'):
|
|
self.remove_contact(contact.jid, account, backend=True)
|
|
|
|
uf_show = helpers.get_uf_show(show)
|
|
|
|
# print status in chat window and update status/GPG image
|
|
ctrl = app.interface.msg_win_mgr.get_control(contact.jid, account)
|
|
if ctrl and not ctrl.is_groupchat:
|
|
ctrl.contact = app.contacts.get_contact_with_highest_priority(
|
|
account, contact.jid)
|
|
ctrl.update_status_display(name, uf_show, status_message)
|
|
|
|
if contact.resource:
|
|
ctrl = app.interface.msg_win_mgr.get_control(fjid, account)
|
|
if ctrl:
|
|
ctrl.update_status_display(name, uf_show, status_message)
|
|
|
|
# Delete pep if needed
|
|
keep_pep = any(c.show not in ('error', 'offline') for c in
|
|
contact_instances)
|
|
if not keep_pep and contact.jid != app.get_jid_from_account(account) \
|
|
and not contact.is_groupchat:
|
|
self.delete_pep(contact.jid, account)
|
|
|
|
# Redraw everything and select the sender
|
|
self.adjust_and_draw_contact_context(contact.jid, account)
|
|
|
|
|
|
def on_status_changed(self, account, show):
|
|
"""
|
|
The core tells us that our status has changed
|
|
"""
|
|
if account not in app.contacts.get_accounts():
|
|
return
|
|
child_iterA = self._get_account_iter(account, self.model)
|
|
self_resource = app.connections[account].get_own_jid().resource
|
|
self_contact = app.contacts.get_contact(account,
|
|
app.get_jid_from_account(account), resource=self_resource)
|
|
if self_contact:
|
|
status_message = app.connections[account].status_message
|
|
self.chg_contact_status(self_contact, show, status_message, account)
|
|
self.set_account_status_icon(account)
|
|
if show == 'offline':
|
|
if self.quit_on_next_offline > -1:
|
|
# we want to quit, we are waiting for all accounts to be offline
|
|
self.quit_on_next_offline -= 1
|
|
if self.quit_on_next_offline < 1:
|
|
# all accounts offline, quit
|
|
self.quit_gtkgui_interface()
|
|
else:
|
|
# No need to redraw contacts if we're quitting
|
|
if child_iterA:
|
|
self.model[child_iterA][Column.AVATAR_IMG] = None
|
|
for jid in list(app.contacts.get_jid_list(account)):
|
|
lcontact = app.contacts.get_contacts(account, jid)
|
|
ctrl = app.interface.msg_win_mgr.get_gc_control(jid,
|
|
account)
|
|
for contact in [c for c in lcontact if (
|
|
(c.show != 'offline' or c.is_transport()) and not ctrl)]:
|
|
self.chg_contact_status(contact, 'offline', '', account)
|
|
if app.interface.systray_enabled:
|
|
app.interface.systray.change_status(show)
|
|
self._status_selector.update()
|
|
|
|
def change_status(self, _widget, account, status):
|
|
app.interface.change_account_status(account, status=status)
|
|
|
|
def get_show(self, lcontact):
|
|
prio = lcontact[0].priority
|
|
show = lcontact[0].show
|
|
for u in lcontact:
|
|
if u.priority > prio:
|
|
prio = u.priority
|
|
show = u.show
|
|
return show
|
|
|
|
def on_message_window_delete(self, win_mgr, msg_win):
|
|
if app.settings.get('one_message_window') == 'always_with_roster':
|
|
self.show_roster_vbox(True)
|
|
resize_window(self.window,
|
|
app.settings.get('roster_width'),
|
|
app.settings.get('roster_height'))
|
|
|
|
def close_all_from_dict(self, dic):
|
|
"""
|
|
Close all the windows in the given dictionary
|
|
"""
|
|
for w in list(dic.values()):
|
|
if isinstance(w, dict):
|
|
self.close_all_from_dict(w)
|
|
else:
|
|
try:
|
|
w.window.destroy()
|
|
except (AttributeError, RuntimeError):
|
|
w.destroy()
|
|
|
|
def close_all(self, account, force=False):
|
|
"""
|
|
Close all the windows from an account. If force is True, do not ask
|
|
confirmation before closing chat/gc windows
|
|
"""
|
|
if account in app.interface.instances:
|
|
self.close_all_from_dict(app.interface.instances[account])
|
|
for ctrl in app.interface.msg_win_mgr.get_controls(acct=account):
|
|
ctrl.parent_win.remove_tab(ctrl, ctrl.parent_win.CLOSE_CLOSE_BUTTON,
|
|
force=force)
|
|
|
|
def on_roster_window_delete_event(self, widget, event):
|
|
"""
|
|
Main window X button was clicked
|
|
"""
|
|
if not app.settings.get('quit_on_roster_x_button') and (
|
|
(app.interface.systray_enabled and app.settings.get('trayicon') != \
|
|
'on_event') or app.settings.get('allow_hide_roster')):
|
|
save_roster_position(self.window)
|
|
if os.name == 'nt' or app.settings.get('hide_on_roster_x_button'):
|
|
self.window.hide()
|
|
else:
|
|
self.window.iconify()
|
|
elif app.settings.get('quit_on_roster_x_button'):
|
|
self.on_quit_request()
|
|
else:
|
|
def _on_ok(is_checked):
|
|
if is_checked:
|
|
app.settings.set('quit_on_roster_x_button', True)
|
|
self.on_quit_request()
|
|
ConfirmationCheckDialog(
|
|
_('Quit Gajim'),
|
|
_('You are about to quit Gajim'),
|
|
_('Are you sure you want to quit Gajim?'),
|
|
_('_Always quit when closing Gajim'),
|
|
[DialogButton.make('Cancel'),
|
|
DialogButton.make('Remove',
|
|
text=_('_Quit'),
|
|
callback=_on_ok)]).show()
|
|
return True # Do NOT destroy the window
|
|
|
|
def prepare_quit(self):
|
|
if self.save_done:
|
|
return
|
|
msgwin_width_adjust = 0
|
|
|
|
# in case show_roster_on_start is False and roster is never shown
|
|
# window.window is None
|
|
if self.window.get_window() is not None:
|
|
save_roster_position(self.window)
|
|
width, height = self.window.get_size()
|
|
app.settings.set('roster_width', width)
|
|
app.settings.set('roster_height', height)
|
|
if not self.xml.get_object('roster_vbox2').get_property('visible'):
|
|
# The roster vbox is hidden, so the message window is larger
|
|
# then we want to save (i.e. the window will grow every startup)
|
|
# so adjust.
|
|
msgwin_width_adjust = -1 * width
|
|
app.settings.set('last_roster_visible',
|
|
self.window.get_property('visible'))
|
|
app.interface.msg_win_mgr.save_opened_controls()
|
|
app.interface.msg_win_mgr.shutdown(msgwin_width_adjust)
|
|
|
|
app.settings.set('collapsed_rows', '\t'.join(self.collapsed_rows))
|
|
app.interface.save_config()
|
|
for account in app.connections:
|
|
app.connections[account].quit(True)
|
|
self.close_all(account)
|
|
if app.interface.systray_enabled:
|
|
app.interface.hide_systray()
|
|
self.save_done = True
|
|
|
|
def quit_gtkgui_interface(self):
|
|
"""
|
|
When we quit the gtk interface - exit gtk
|
|
"""
|
|
self.prepare_quit()
|
|
self.application.quit()
|
|
|
|
def on_quit_request(self, widget=None):
|
|
"""
|
|
User wants to quit. Check if he should be warned about messages pending.
|
|
Terminate all sessions and send offline to all connected account. We do
|
|
NOT really quit gajim here
|
|
"""
|
|
accounts = list(app.connections.keys())
|
|
get_msg = False
|
|
for acct in accounts:
|
|
if app.account_is_available(acct):
|
|
get_msg = True
|
|
break
|
|
|
|
def on_continue3(message):
|
|
self.quit_on_next_offline = 0
|
|
accounts_to_disconnect = []
|
|
for acct in accounts:
|
|
if app.account_is_available(acct):
|
|
self.quit_on_next_offline += 1
|
|
accounts_to_disconnect.append(acct)
|
|
|
|
if not self.quit_on_next_offline:
|
|
# all accounts offline, quit
|
|
self.quit_gtkgui_interface()
|
|
return
|
|
|
|
for acct in accounts_to_disconnect:
|
|
self.send_status(acct, 'offline', message)
|
|
|
|
def on_continue2(message):
|
|
if 'file_transfers' not in app.interface.instances:
|
|
on_continue3(message)
|
|
return
|
|
# check if there is an active file transfer
|
|
from gajim.common.modules.bytestream import is_transfer_active
|
|
files_props = app.interface.instances['file_transfers'].\
|
|
files_props
|
|
transfer_active = False
|
|
for x in files_props:
|
|
for y in files_props[x]:
|
|
if is_transfer_active(files_props[x][y]):
|
|
transfer_active = True
|
|
break
|
|
|
|
if transfer_active:
|
|
ConfirmationDialog(
|
|
_('Stop File Transfers'),
|
|
_('You still have running file transfers'),
|
|
_('If you quit now, the file(s) being transferred will '
|
|
'be lost.\n'
|
|
'Do you still want to quit?'),
|
|
[DialogButton.make('Cancel'),
|
|
DialogButton.make('Remove',
|
|
text=_('_Quit'),
|
|
callback=on_continue3,
|
|
args=[message])]).show()
|
|
return
|
|
on_continue3(message)
|
|
|
|
def on_continue(message):
|
|
if message is None:
|
|
# user pressed Cancel to change status message dialog
|
|
return
|
|
# check if we have unread messages
|
|
unread = app.events.get_nb_events()
|
|
|
|
for event in app.events.get_all_events(['printed_gc_msg']):
|
|
contact = app.contacts.get_groupchat_contact(event.account,
|
|
event.jid)
|
|
if contact is None or not contact.can_notify():
|
|
unread -= 1
|
|
|
|
# check if we have recent messages
|
|
recent = False
|
|
for win in app.interface.msg_win_mgr.windows():
|
|
for ctrl in win.controls():
|
|
fjid = ctrl.get_full_jid()
|
|
if fjid in app.last_message_time[ctrl.account]:
|
|
if time.time() - app.last_message_time[ctrl.account][
|
|
fjid] < 2:
|
|
recent = True
|
|
break
|
|
if recent:
|
|
break
|
|
|
|
if unread or recent:
|
|
ConfirmationDialog(
|
|
_('Unread Messages'),
|
|
_('You still have unread messages'),
|
|
_('Messages will only be available for reading them later '
|
|
'if storing chat history is enabled and if the contact '
|
|
'is in your contact list.'),
|
|
[DialogButton.make('Cancel'),
|
|
DialogButton.make('Remove',
|
|
text=_('_Quit'),
|
|
callback=on_continue2,
|
|
args=[message])]).show()
|
|
return
|
|
on_continue2(message)
|
|
|
|
if get_msg and ask_for_status_message('offline'):
|
|
open_window('StatusChange',
|
|
status='offline',
|
|
callback=on_continue,
|
|
show_pep=False)
|
|
else:
|
|
on_continue('')
|
|
|
|
def _nec_presence_received(self, obj):
|
|
account = obj.conn.name
|
|
jid = obj.jid
|
|
|
|
if obj.need_add_in_roster:
|
|
self.add_contact(jid, account)
|
|
|
|
jid_list = app.contacts.get_jid_list(account)
|
|
if jid in jid_list or jid == app.get_jid_from_account(account):
|
|
if not app.jid_is_transport(jid) and len(obj.contact_list) == 1:
|
|
if obj.old_show == 0 and obj.new_show > 1:
|
|
GLib.timeout_add_seconds(5, self.remove_newly_added, jid,
|
|
account)
|
|
elif obj.old_show > 1 and obj.new_show == 0 and \
|
|
obj.conn.state.is_available:
|
|
GLib.timeout_add_seconds(5, self.remove_to_be_removed,
|
|
jid, account)
|
|
|
|
self.draw_contact(jid, account)
|
|
|
|
if app.jid_is_transport(jid) and jid in jid_list:
|
|
# It must be an agent
|
|
# Update existing iter and group counting
|
|
self.draw_contact(jid, account)
|
|
self.draw_group(_('Transports'), account)
|
|
|
|
if obj.contact:
|
|
self.chg_contact_status(obj.contact, obj.show, obj.status, account)
|
|
|
|
if obj.popup:
|
|
ctrl = app.interface.msg_win_mgr.search_control(jid, account)
|
|
if ctrl:
|
|
GLib.idle_add(ctrl.parent_win.set_active_tab, ctrl)
|
|
else:
|
|
ctrl = app.interface.new_chat(obj.contact, account)
|
|
if app.events.get_events(account, obj.jid):
|
|
ctrl.read_queue()
|
|
|
|
def _nec_roster_received(self, obj):
|
|
if obj.received_from_server:
|
|
self.fill_contacts_and_groups_dicts(obj.roster, obj.conn.name)
|
|
self.add_account_contacts(obj.conn.name)
|
|
self.fire_up_unread_messages_events(obj.conn.name)
|
|
else:
|
|
# add self contact
|
|
account = obj.conn.name
|
|
self_jid = app.get_jid_from_account(account)
|
|
if self_jid not in app.contacts.get_jid_list(account):
|
|
sha = app.settings.get_account_setting(account, 'avatar_sha')
|
|
contact = app.contacts.create_contact(
|
|
jid=self_jid, account=account, name=app.nicks[account],
|
|
groups=['self_contact'], show='offline', sub='both',
|
|
ask='none', avatar_sha=sha)
|
|
app.contacts.add_contact(account, contact)
|
|
self.add_contact(self_jid, account)
|
|
|
|
if app.settings.get('remember_opened_chat_controls'):
|
|
account = obj.conn.name
|
|
controls = app.settings.get_account_setting(
|
|
account, 'opened_chat_controls')
|
|
if controls:
|
|
for jid in controls.split(','):
|
|
contact = \
|
|
app.contacts.get_contact_with_highest_priority(
|
|
account, jid)
|
|
if not contact:
|
|
contact = self.add_to_not_in_the_roster(
|
|
account, jid)
|
|
app.interface.on_open_chat_window(
|
|
None, contact, account)
|
|
app.settings.set_account_setting(
|
|
account, 'opened_chat_controls', '')
|
|
GLib.idle_add(self.refilter_shown_roster_items)
|
|
|
|
def _nec_anonymous_auth(self, obj):
|
|
"""
|
|
This event is raised when our JID changed (most probably because we use
|
|
anonymous account. We update contact and roster entry in this case
|
|
"""
|
|
self.rename_self_contact(obj.old_jid, obj.new_jid, obj.conn.name)
|
|
|
|
def _nec_our_show(self, event):
|
|
if event.show == 'offline':
|
|
self.application.set_account_actions_state(event.account)
|
|
self.application.update_app_actions_state()
|
|
|
|
self.on_status_changed(event.account, event.show)
|
|
|
|
def _nec_connection_type(self, obj):
|
|
self.draw_account(obj.conn.name)
|
|
|
|
def _nec_agent_removed(self, obj):
|
|
for jid in obj.jid_list:
|
|
self.remove_contact(jid, obj.conn.name, backend=True)
|
|
|
|
def _on_mood_received(self, event):
|
|
if event.is_self_message:
|
|
self.draw_account(event.account)
|
|
self._draw_pep(event.account, event.jid, PEPEventType.MOOD)
|
|
|
|
def _on_activity_received(self, event):
|
|
if event.is_self_message:
|
|
self.draw_account(event.account)
|
|
self._draw_pep(event.account, event.jid, PEPEventType.ACTIVITY)
|
|
|
|
def _on_tune_received(self, event):
|
|
if event.is_self_message:
|
|
self.draw_account(event.account)
|
|
self._draw_pep(event.account, event.jid, PEPEventType.TUNE)
|
|
|
|
def _on_location_received(self, event):
|
|
if event.is_self_message:
|
|
self.draw_account(event.account)
|
|
self._draw_pep(event.account, event.jid, PEPEventType.LOCATION)
|
|
|
|
def _on_nickname_received(self, event):
|
|
self.draw_contact(event.jid, event.account)
|
|
|
|
def _nec_update_avatar(self, obj):
|
|
app.log('avatar').debug('Draw roster avatar: %s', obj.jid)
|
|
self.draw_avatar(obj.jid, obj.account)
|
|
|
|
def _nec_muc_subject_received(self, event):
|
|
self.draw_contact(event.room_jid, event.account)
|
|
|
|
def _on_muc_disco_update(self, event):
|
|
self.draw_contact(str(event.room_jid), event.account)
|
|
|
|
def _on_bookmarks_received(self, event):
|
|
con = app.connections[event.account]
|
|
for bookmark in con.get_module('Bookmarks').bookmarks:
|
|
self.draw_contact(str(bookmark.jid), event.account)
|
|
|
|
def _nec_metacontacts_received(self, obj):
|
|
self.redraw_metacontacts(obj.conn.name)
|
|
|
|
def _nec_signed_in(self, obj):
|
|
self.application.set_account_actions_state(obj.conn.name, True)
|
|
self.application.update_app_actions_state()
|
|
self.draw_account(obj.conn.name)
|
|
|
|
def _nec_decrypted_message_received(self, obj):
|
|
if not obj.msgtxt:
|
|
return True
|
|
if obj.properties.type.value not in ('normal', 'chat'):
|
|
return
|
|
|
|
if obj.popup and not obj.session.control:
|
|
contact = app.contacts.get_contact(obj.conn.name, obj.jid)
|
|
obj.session.control = app.interface.new_chat(contact,
|
|
obj.conn.name, session=obj.session)
|
|
if app.events.get_events(obj.conn.name, obj.fjid):
|
|
obj.session.control.read_queue()
|
|
|
|
if not obj.properties.is_muc_pm and obj.show_in_roster:
|
|
self.draw_contact(obj.jid, obj.conn.name)
|
|
self.show_title() # we show the * or [n]
|
|
# Select the big brother contact in roster, it's visible because it
|
|
# has events.
|
|
family = app.contacts.get_metacontacts_family(obj.conn.name,
|
|
obj.jid)
|
|
if family:
|
|
_nearby_family, bb_jid, bb_account = \
|
|
app.contacts.get_nearby_family_and_big_brother(family,
|
|
obj.conn.name)
|
|
else:
|
|
bb_jid, bb_account = obj.jid, obj.conn.name
|
|
self.select_contact(bb_jid, bb_account)
|
|
|
|
################################################################################
|
|
### Menu and GUI callbacks
|
|
### FIXME: order callbacks in itself...
|
|
################################################################################
|
|
|
|
def on_info(self, widget, contact, account):
|
|
"""
|
|
Call vcard_information_window class to display contact's information
|
|
"""
|
|
if app.connections[account].is_zeroconf:
|
|
self.on_info_zeroconf(widget, contact, account)
|
|
return
|
|
|
|
info = app.interface.instances[account]['infos']
|
|
if contact.jid in info:
|
|
info[contact.jid].window.present()
|
|
else:
|
|
info[contact.jid] = vcard.VcardWindow(contact, account)
|
|
|
|
def on_info_zeroconf(self, widget, contact, account):
|
|
info = app.interface.instances[account]['infos']
|
|
if contact.jid in info:
|
|
info[contact.jid].window.present()
|
|
else:
|
|
contact = app.contacts.get_first_contact_from_jid(account,
|
|
contact.jid)
|
|
if contact.show in ('offline', 'error'):
|
|
# don't show info on offline contacts
|
|
return
|
|
info[contact.jid] = vcard.ZeroconfVcardWindow(contact, account)
|
|
|
|
def on_edit_agent(self, widget, contact, account):
|
|
"""
|
|
When we want to modify the agent registration
|
|
"""
|
|
ServiceRegistration(account, contact.jid)
|
|
|
|
def on_remove_agent(self, widget, list_):
|
|
"""
|
|
When an agent is requested to be removed. list_ is a list of (contact,
|
|
account) tuple
|
|
"""
|
|
for (contact, account) in list_:
|
|
if app.settings.get_account_setting(account, 'hostname') == \
|
|
contact.jid:
|
|
# We remove the server contact
|
|
# remove it from treeview
|
|
app.connections[account].get_module('Presence').unsubscribe(contact.jid)
|
|
self.remove_contact(contact.jid, account, backend=True)
|
|
return
|
|
|
|
def remove():
|
|
for (contact, account) in list_:
|
|
full_jid = contact.get_full_jid()
|
|
app.connections[account].get_module('Gateway').unsubscribe(full_jid)
|
|
# remove transport from treeview
|
|
self.remove_contact(contact.jid, account, backend=True)
|
|
|
|
# Check if there are unread events from some contacts
|
|
has_unread_events = False
|
|
for (contact, account) in list_:
|
|
for jid in app.events.get_events(account):
|
|
if jid.endswith(contact.jid):
|
|
has_unread_events = True
|
|
break
|
|
if has_unread_events:
|
|
ErrorDialog(
|
|
_('You have unread messages'),
|
|
_('You must read them before removing this transport.'))
|
|
return
|
|
if len(list_) == 1:
|
|
pritext = _('Transport \'%s\' will be removed') % list_[0][0].jid
|
|
sectext = _('You will no longer be able to send and receive '
|
|
'messages from and to contacts using this transport.')
|
|
else:
|
|
pritext = _('Transports will be removed')
|
|
jids = ''
|
|
for (contact, account) in list_:
|
|
jids += '\n ' + contact.get_shown_name() + ','
|
|
jids = jids[:-1] + '.'
|
|
sectext = _('You will no longer be able to send and receive '
|
|
'messages from and to contacts using these '
|
|
'transports:\n%s') % jids
|
|
ConfirmationDialog(
|
|
_('Remove Transport'),
|
|
pritext,
|
|
sectext,
|
|
[DialogButton.make('Cancel'),
|
|
DialogButton.make('Remove',
|
|
callback=remove)],
|
|
transient_for=self.window).show()
|
|
|
|
def _nec_blocking(self, obj):
|
|
for jid in obj.changed:
|
|
self.draw_contact(str(jid), obj.conn.name)
|
|
|
|
def on_block(self, widget, list_):
|
|
"""
|
|
When clicked on the 'block' button in context menu. list_ is a list of
|
|
(contact, account)
|
|
"""
|
|
def _block_it(is_checked=None, report=None):
|
|
if is_checked is not None: # Dialog has been shown
|
|
if is_checked:
|
|
app.settings.set('confirm_block', 'no')
|
|
else:
|
|
app.settings.set('confirm_block', 'yes')
|
|
|
|
accounts = []
|
|
for _, account in list_:
|
|
con = app.connections[account]
|
|
if con.get_module('Blocking').supported:
|
|
accounts.append(account)
|
|
|
|
for acct in accounts:
|
|
l_ = [i[0] for i in list_ if i[1] == acct]
|
|
con = app.connections[acct]
|
|
jid_list = [contact.jid for contact in l_]
|
|
con.get_module('Blocking').block(jid_list, report)
|
|
for contact in l_:
|
|
app.events.remove_events(acct, contact.jid)
|
|
ctrl = app.interface.msg_win_mgr.get_control(
|
|
contact.jid, acct)
|
|
if ctrl:
|
|
ctrl.parent_win.remove_tab(
|
|
ctrl, ctrl.parent_win.CLOSE_COMMAND, force=True)
|
|
if contact.show == 'not in roster':
|
|
self.remove_contact(contact.jid, acct, force=True,
|
|
backend=True)
|
|
return
|
|
self.draw_contact(contact.jid, acct)
|
|
|
|
# Check if confirmation is needed for blocking
|
|
confirm_block = app.settings.get('confirm_block')
|
|
if confirm_block == 'no':
|
|
_block_it()
|
|
return
|
|
|
|
ConfirmationCheckDialog(
|
|
_('Block Contact'),
|
|
_('Really block this contact?'),
|
|
_('You will appear offline for this contact and you '
|
|
'will not receive further messages.'),
|
|
_('_Do not ask again'),
|
|
[DialogButton.make('Cancel'),
|
|
DialogButton.make('OK',
|
|
text=_('_Report Spam'),
|
|
callback=_block_it,
|
|
kwargs={'report': 'spam'}),
|
|
DialogButton.make('Remove',
|
|
text=_('_Block'),
|
|
callback=_block_it)],
|
|
modal=False).show()
|
|
|
|
def on_unblock(self, widget, list_):
|
|
"""
|
|
When clicked on the 'unblock' button in context menu.
|
|
"""
|
|
accounts = []
|
|
for _, account in list_:
|
|
con = app.connections[account]
|
|
if con.get_module('Blocking').supported:
|
|
accounts.append(account)
|
|
|
|
for acct in accounts:
|
|
l_ = [i[0] for i in list_ if i[1] == acct]
|
|
con = app.connections[acct]
|
|
jid_list = [contact.jid for contact in l_]
|
|
con.get_module('Blocking').unblock(jid_list)
|
|
for contact in l_:
|
|
self.draw_contact(contact.jid, acct)
|
|
|
|
def on_rename(self, widget, row_type, jid, account):
|
|
# This function is called either by F2 or by Rename menuitem
|
|
if 'rename' in app.interface.instances:
|
|
app.interface.instances['rename'].dialog.present()
|
|
return
|
|
|
|
# Account is offline, don't allow to rename
|
|
if not app.account_is_available(account):
|
|
return
|
|
if row_type in ('contact', 'agent'):
|
|
# It's jid
|
|
title = _('Rename Contact')
|
|
text = _('Rename contact %s?') % jid
|
|
sec_text = _('Please enter a new nickname')
|
|
old_text = app.contacts.get_contact_with_highest_priority(account,
|
|
jid).name
|
|
elif row_type == 'group':
|
|
if jid in helpers.special_groups + (_('General'),):
|
|
return
|
|
old_text = jid
|
|
title = _('Rename Group')
|
|
text = _('Rename group %s?') % GLib.markup_escape_text(jid)
|
|
sec_text = _('Please enter a new name')
|
|
|
|
def _on_renamed(new_text, account, row_type, jid, old_text):
|
|
if row_type in ('contact', 'agent'):
|
|
if old_text == new_text:
|
|
return
|
|
contacts = app.contacts.get_contacts(account, jid)
|
|
for contact in contacts:
|
|
contact.name = new_text
|
|
con = app.connections[account]
|
|
con.get_module('Roster').update_contact(
|
|
jid, new_text, contacts[0].groups)
|
|
self.draw_contact(jid, account)
|
|
# Update opened chats
|
|
for ctrl in app.interface.msg_win_mgr.get_controls(jid,
|
|
account):
|
|
ctrl.update_ui()
|
|
win = app.interface.msg_win_mgr.get_window(jid, account)
|
|
win.redraw_tab(ctrl)
|
|
win.show_title()
|
|
elif row_type == 'group':
|
|
# In Column.JID column, we hold the group name (which is not escaped)
|
|
self.rename_group(old_text, new_text, account)
|
|
|
|
InputDialog(
|
|
title,
|
|
text,
|
|
sec_text,
|
|
[DialogButton.make('Cancel'),
|
|
DialogButton.make('Accept',
|
|
text=_('_Rename'),
|
|
callback=_on_renamed,
|
|
args=[account,
|
|
row_type,
|
|
jid,
|
|
old_text])],
|
|
input_str=old_text,
|
|
transient_for=self.window).show()
|
|
|
|
def on_remove_group_item_activated(self, widget, group, account):
|
|
def _on_ok(is_checked):
|
|
for contact in app.contacts.get_contacts_from_group(account,
|
|
group):
|
|
if not is_checked:
|
|
self.remove_contact_from_groups(contact.jid, account,
|
|
[group])
|
|
else:
|
|
app.connections[account].get_module(
|
|
'Presence').unsubscribe(contact.jid)
|
|
self.remove_contact(contact.jid, account, backend=True)
|
|
|
|
ConfirmationCheckDialog(
|
|
_('Remove Group'),
|
|
_('Remove Group'),
|
|
_('Do you want to remove %s from the contact list?') % group,
|
|
_('_Also remove all contacts of this group from contact list'),
|
|
[DialogButton.make('Cancel'),
|
|
DialogButton.make('Remove',
|
|
callback=_on_ok)]).show()
|
|
|
|
def on_edit_groups(self, widget, list_):
|
|
dialogs.EditGroupsDialog(list_)
|
|
|
|
def on_disconnect(self, widget, jid, account):
|
|
"""
|
|
When disconnect menuitem is activated: disconnect from room
|
|
"""
|
|
if jid in app.interface.minimized_controls[account]:
|
|
ctrl = app.interface.minimized_controls[account][jid]
|
|
ctrl.leave()
|
|
self.remove_groupchat(jid, account)
|
|
|
|
def on_send_single_message_menuitem_activate(self, widget, account,
|
|
contact=None):
|
|
if contact is None:
|
|
SingleMessageWindow(account, action='send')
|
|
elif isinstance(contact, list):
|
|
SingleMessageWindow(account, contact, 'send')
|
|
else:
|
|
jid = contact.jid
|
|
if contact.jid == app.get_jid_from_account(account):
|
|
jid += '/' + contact.resource
|
|
SingleMessageWindow(account, jid, 'send')
|
|
|
|
def on_send_file_menuitem_activate(self, widget, contact, account,
|
|
resource=None):
|
|
app.interface.instances['file_transfers'].show_file_send_request(
|
|
account, contact)
|
|
|
|
def on_invite_to_room(self,
|
|
_widget,
|
|
list_,
|
|
room_jid,
|
|
room_account,
|
|
resource=None):
|
|
"""
|
|
Resource parameter MUST NOT be used if more than one contact in list
|
|
"""
|
|
gc_control = app.get_groupchat_control(room_account, room_jid)
|
|
if gc_control is None:
|
|
return
|
|
|
|
for contact, _ in list_:
|
|
contact_jid = contact.jid
|
|
if resource: # we MUST have one contact only in list_
|
|
contact_jid += '/' + resource
|
|
gc_control.invite(contact_jid)
|
|
|
|
def on_all_groupchat_maximized(self, widget, group_list):
|
|
for (contact, account) in group_list:
|
|
self.on_groupchat_maximized(widget, contact.jid, account)
|
|
|
|
def on_groupchat_maximized(self, widget, jid, account):
|
|
"""
|
|
When a groupchat is maximized
|
|
"""
|
|
if not jid in app.interface.minimized_controls[account]:
|
|
# Already opened?
|
|
gc_control = app.interface.msg_win_mgr.get_gc_control(jid,
|
|
account)
|
|
if gc_control:
|
|
mw = app.interface.msg_win_mgr.get_window(jid, account)
|
|
mw.set_active_tab(gc_control)
|
|
return
|
|
ctrl = app.interface.minimized_controls[account][jid]
|
|
mw = app.interface.msg_win_mgr.get_window(jid, account)
|
|
if not mw:
|
|
mw = app.interface.msg_win_mgr.create_window(
|
|
ctrl.contact, ctrl.account, ctrl.type)
|
|
id_ = mw.window.connect('motion-notify-event',
|
|
ctrl._on_window_motion_notify)
|
|
ctrl.handlers[id_] = mw.window
|
|
ctrl.parent_win = mw
|
|
ctrl.on_groupchat_maximize()
|
|
mw.new_tab(ctrl)
|
|
mw.set_active_tab(ctrl)
|
|
self.remove_groupchat(jid, account, maximize=True)
|
|
|
|
def on_groupchat_rename(self, _widget, jid, account):
|
|
def _on_rename(new_name):
|
|
con = app.connections[account]
|
|
con.get_module('Bookmarks').modify(jid, name=new_name)
|
|
|
|
contact = app.contacts.get_first_contact_from_jid(account, jid)
|
|
name = contact.get_shown_name()
|
|
|
|
InputDialog(
|
|
_('Rename Group Chat'),
|
|
_('Rename Group Chat'),
|
|
_('Please enter a new name for this group chat'),
|
|
[DialogButton.make('Cancel'),
|
|
DialogButton.make('Accept',
|
|
text=_('_Rename'),
|
|
callback=_on_rename)],
|
|
input_str=name,
|
|
transient_for=self.window).show()
|
|
|
|
def on_change_status_message_activate(self, _widget, account):
|
|
app.interface.change_account_status(account)
|
|
|
|
def on_add_to_roster(self, widget, contact, account):
|
|
AddNewContactWindow(account, contact.jid, contact.name)
|
|
|
|
def on_roster_treeview_key_press_event(self, widget, event):
|
|
"""
|
|
When a key is pressed in the treeviews
|
|
"""
|
|
if event.keyval == Gdk.KEY_Escape:
|
|
if self.rfilter_enabled:
|
|
self.disable_rfilter()
|
|
else:
|
|
self.tree.get_selection().unselect_all()
|
|
elif event.keyval == Gdk.KEY_F2:
|
|
treeselection = self.tree.get_selection()
|
|
model, list_of_paths = treeselection.get_selected_rows()
|
|
if len(list_of_paths) != 1:
|
|
return
|
|
path = list_of_paths[0]
|
|
type_ = model[path][Column.TYPE]
|
|
if type_ in ('contact', 'group', 'agent'):
|
|
jid = model[path][Column.JID]
|
|
account = model[path][Column.ACCOUNT]
|
|
self.on_rename(widget, type_, jid, account)
|
|
|
|
elif event.keyval == Gdk.KEY_Delete:
|
|
treeselection = self.tree.get_selection()
|
|
model, list_of_paths = treeselection.get_selected_rows()
|
|
if not list_of_paths:
|
|
return
|
|
type_ = model[list_of_paths[0]][Column.TYPE]
|
|
account = model[list_of_paths[0]][Column.ACCOUNT]
|
|
if type_ in ('account', 'group', 'self_contact') or \
|
|
account == app.ZEROCONF_ACC_NAME:
|
|
return
|
|
list_ = []
|
|
for path in list_of_paths:
|
|
if model[path][Column.TYPE] != type_:
|
|
return
|
|
jid = model[path][Column.JID]
|
|
account = model[path][Column.ACCOUNT]
|
|
if not app.account_is_available(account):
|
|
continue
|
|
contact = app.contacts.get_contact_with_highest_priority(
|
|
account, jid)
|
|
list_.append((contact, account))
|
|
if not list_:
|
|
return
|
|
if type_ == 'contact':
|
|
self.on_req_usub(widget, list_)
|
|
elif type_ == 'agent':
|
|
self.on_remove_agent(widget, list_)
|
|
|
|
elif not (event.get_state() &
|
|
(Gdk.ModifierType.CONTROL_MASK |
|
|
Gdk.ModifierType.MOD1_MASK)):
|
|
num = Gdk.keyval_to_unicode(event.keyval)
|
|
if num and num > 31:
|
|
# if we got unicode symbol without ctrl / alt
|
|
self.enable_rfilter(chr(num))
|
|
|
|
elif event.get_state() & Gdk.ModifierType.CONTROL_MASK and \
|
|
event.get_state() & Gdk.ModifierType.SHIFT_MASK and \
|
|
event.keyval == Gdk.KEY_U:
|
|
self.enable_rfilter('')
|
|
self.rfilter_entry.event(event)
|
|
|
|
elif event.keyval == Gdk.KEY_Left:
|
|
treeselection = self.tree.get_selection()
|
|
model, list_of_paths = treeselection.get_selected_rows()
|
|
if len(list_of_paths) != 1:
|
|
return
|
|
path = list_of_paths[0]
|
|
iter_ = model.get_iter(path)
|
|
if model.iter_has_child(iter_) and self.tree.row_expanded(path):
|
|
self.tree.collapse_row(path)
|
|
return True
|
|
if path.get_depth() > 1:
|
|
self.tree.set_cursor(path[:-1])
|
|
return True
|
|
elif event.keyval == Gdk.KEY_Right:
|
|
treeselection = self.tree.get_selection()
|
|
model, list_of_paths = treeselection.get_selected_rows()
|
|
if len(list_of_paths) != 1:
|
|
return
|
|
path = list_of_paths[0]
|
|
iter_ = model.get_iter(path)
|
|
if model.iter_has_child(iter_):
|
|
self.tree.expand_row(path, False)
|
|
return True
|
|
|
|
def accel_group_func(self, accel_group, acceleratable, keyval, modifier):
|
|
# CTRL mask
|
|
if modifier & Gdk.ModifierType.CONTROL_MASK:
|
|
if keyval == Gdk.KEY_s: # CTRL + s
|
|
app.interface.change_status()
|
|
return True
|
|
if keyval == Gdk.KEY_k: # CTRL + k
|
|
self.enable_rfilter('')
|
|
|
|
def on_roster_treeview_button_press_event(self, widget, event):
|
|
try:
|
|
pos = self.tree.get_path_at_pos(int(event.x), int(event.y))
|
|
path, x = pos[0], pos[2]
|
|
except TypeError:
|
|
self.tree.get_selection().unselect_all()
|
|
return False
|
|
|
|
if event.button == 3: # Right click
|
|
try:
|
|
model, list_of_paths = self.tree.get_selection().\
|
|
get_selected_rows()
|
|
except TypeError:
|
|
list_of_paths = []
|
|
if path not in list_of_paths:
|
|
self.tree.get_selection().unselect_all()
|
|
self.tree.get_selection().select_path(path)
|
|
return self.show_treeview_menu(event)
|
|
|
|
if event.button == 2: # Middle click
|
|
try:
|
|
model, list_of_paths = self.tree.get_selection().\
|
|
get_selected_rows()
|
|
except TypeError:
|
|
list_of_paths = []
|
|
if list_of_paths != [path]:
|
|
self.tree.get_selection().unselect_all()
|
|
self.tree.get_selection().select_path(path)
|
|
type_ = model[path][Column.TYPE]
|
|
if type_ in ('agent', 'contact', 'self_contact', 'groupchat'):
|
|
self.on_row_activated(widget, path)
|
|
elif type_ == 'account':
|
|
account = model[path][Column.ACCOUNT]
|
|
if account != 'all':
|
|
if app.account_is_available(account):
|
|
app.interface.change_account_status(account)
|
|
return True
|
|
|
|
show = helpers.get_global_show()
|
|
if show == 'offline':
|
|
return True
|
|
app.interface.change_status()
|
|
return True
|
|
|
|
if event.button == 1: # Left click
|
|
model = self.modelfilter
|
|
type_ = model[path][Column.TYPE]
|
|
# x_min is the x start position of status icon column
|
|
if app.settings.get('avatar_position_in_roster') == 'left':
|
|
x_min = AvatarSize.ROSTER
|
|
else:
|
|
x_min = 0
|
|
|
|
if type_ == 'group' and x < 27:
|
|
# first cell in 1st column (the arrow SINGLE clicked)
|
|
if self.tree.row_expanded(path):
|
|
self.tree.collapse_row(path)
|
|
else:
|
|
self.expand_group_row(path)
|
|
|
|
elif type_ == 'contact' and x_min < x < x_min + 27:
|
|
if self.tree.row_expanded(path):
|
|
self.tree.collapse_row(path)
|
|
else:
|
|
self.tree.expand_row(path, False)
|
|
|
|
def expand_group_row(self, path):
|
|
self.tree.expand_row(path, False)
|
|
iter_ = self.modelfilter.get_iter(path)
|
|
child_iter = self.modelfilter.iter_children(iter_)
|
|
while child_iter:
|
|
type_ = self.modelfilter[child_iter][Column.TYPE]
|
|
account = self.modelfilter[child_iter][Column.ACCOUNT]
|
|
group = self.modelfilter[child_iter][Column.JID]
|
|
if type_ == 'group' and account + group not in self.collapsed_rows:
|
|
self.expand_group_row(self.modelfilter.get_path(child_iter))
|
|
child_iter = self.modelfilter.iter_next(child_iter)
|
|
|
|
def on_req_usub(self, widget, list_):
|
|
"""
|
|
Remove a contact. list_ is a list of (contact, account) tuples
|
|
"""
|
|
def on_ok(is_checked):
|
|
remove_auth = True
|
|
if len(list_) == 1:
|
|
contact = list_[0][0]
|
|
if contact.sub != 'to' and is_checked:
|
|
remove_auth = False
|
|
for (contact, account) in list_:
|
|
if _('Not in contact list') not in contact.get_shown_groups():
|
|
app.connections[account].get_module('Presence').unsubscribe(contact.jid,
|
|
remove_auth)
|
|
self.remove_contact(contact.jid, account, backend=True)
|
|
if not remove_auth and contact.sub == 'both':
|
|
contact.name = ''
|
|
contact.groups = []
|
|
contact.sub = 'from'
|
|
# we can't see him, but have to set it manually in contact
|
|
contact.show = 'offline'
|
|
app.contacts.add_contact(account, contact)
|
|
self.add_contact(contact.jid, account)
|
|
def on_ok2():
|
|
on_ok(False)
|
|
|
|
if len(list_) == 1:
|
|
contact = list_[0][0]
|
|
title = _('Remove Contact')
|
|
pritext = _('Remove contact from contact list')
|
|
sectext = _('You are about to remove %(name)s (%(jid)s) from '
|
|
'your contact list.\n') % {
|
|
'name': contact.get_shown_name(),
|
|
'jid': contact.jid}
|
|
if contact.sub == 'to':
|
|
ConfirmationDialog(
|
|
title,
|
|
pritext,
|
|
sectext + \
|
|
_('By removing this contact you also remove authorization. '
|
|
'This means the contact will see you as offline.'),
|
|
[DialogButton.make('Cancel'),
|
|
DialogButton.make('Remove',
|
|
callback=on_ok2)]).show()
|
|
elif _('Not in contact list') in contact.get_shown_groups():
|
|
# Contact is not in roster
|
|
ConfirmationDialog(
|
|
title,
|
|
pritext,
|
|
sectext + \
|
|
_('Do you want to continue?'),
|
|
[DialogButton.make('Cancel'),
|
|
DialogButton.make('Remove',
|
|
callback=on_ok2)]).show()
|
|
else:
|
|
ConfirmationCheckDialog(
|
|
title,
|
|
pritext,
|
|
sectext + \
|
|
_('By removing this contact you also remove authorization. '
|
|
'This means the contact will see you as offline.'),
|
|
_('_I want this contact to know my status after removal'),
|
|
[DialogButton.make('Cancel'),
|
|
DialogButton.make('Remove',
|
|
callback=on_ok)],
|
|
modal=False).show()
|
|
else:
|
|
# several contact to remove at the same time
|
|
pritext = _('Remove contacts from contact list')
|
|
jids = ''
|
|
for contact, _account in list_:
|
|
jids += '%(name)s (%(jid)s)\n' % {
|
|
'name': contact.get_shown_name(),
|
|
'jid': contact.jid}
|
|
sectext = _('By removing the following contacts, you will also '
|
|
'remove authorization. This means they will see you '
|
|
'as offline:\n\n%s') % jids
|
|
ConfirmationDialog(
|
|
_('Remove Contacts'),
|
|
pritext,
|
|
sectext,
|
|
[DialogButton.make('Cancel'),
|
|
DialogButton.make('Remove',
|
|
callback=on_ok2)]).show()
|
|
|
|
def on_publish_tune_toggled(self, widget, account):
|
|
active = widget.get_active()
|
|
client = app.get_client(account)
|
|
client.get_module('UserTune').set_enabled(active)
|
|
|
|
def on_publish_location_toggled(self, widget, account):
|
|
active = widget.get_active()
|
|
client = app.get_client(account)
|
|
app.settings.set_account_setting(account, 'publish_location', active)
|
|
|
|
if active:
|
|
location.enable()
|
|
else:
|
|
client = app.get_client(account)
|
|
client.set_user_location(None)
|
|
|
|
client.get_module('Caps').update_caps()
|
|
|
|
def on_add_new_contact(self, widget, account):
|
|
AddNewContactWindow(account)
|
|
|
|
def on_create_gc_activate(self, widget, account):
|
|
"""
|
|
When the create gc menuitem is clicked, show the create gc window
|
|
"""
|
|
app.app.activate_action('create-groupchat',
|
|
GLib.Variant('s', account))
|
|
|
|
def on_show_transports_action(self, action, param):
|
|
app.settings.set('show_transports_group', param.get_boolean())
|
|
action.set_state(param)
|
|
self.refilter_shown_roster_items()
|
|
|
|
def on_execute_command(self, widget, contact, account, resource=None):
|
|
"""
|
|
Execute command. Full JID needed; if it is other contact, resource is
|
|
necessary. Widget is unnecessary, only to be able to make this a
|
|
callback
|
|
"""
|
|
jid = contact.jid
|
|
if resource:
|
|
jid = jid + '/' + resource
|
|
AdHocCommand(account, jid)
|
|
|
|
def on_view_server_info(self, _widget, account):
|
|
app.app.activate_action('%s-server-info' % account,
|
|
GLib.Variant('s', account))
|
|
|
|
def on_roster_window_focus_in_event(self, widget, event):
|
|
# roster received focus, so if we had urgency REMOVE IT
|
|
# NOTE: we do not have to read the message to remove urgency
|
|
# so this functions does that
|
|
set_urgency_hint(widget, False)
|
|
|
|
# if a contact row is selected, update colors (eg. for status msg)
|
|
# because gtk engines may differ in bg when window is selected
|
|
# or not
|
|
if self._last_selected_contact:
|
|
for (jid, account) in self._last_selected_contact:
|
|
self.draw_contact(jid, account, selected=True, focus=True)
|
|
|
|
def on_roster_window_focus_out_event(self, widget, event):
|
|
# if a contact row is selected, update colors (eg. for status msg)
|
|
# because gtk engines may differ in bg when window is selected
|
|
# or not
|
|
if self._last_selected_contact:
|
|
for (jid, account) in self._last_selected_contact:
|
|
self.draw_contact(jid, account, selected=True, focus=False)
|
|
|
|
def on_roster_window_key_press_event(self, widget, event):
|
|
if event.keyval == Gdk.KEY_Escape:
|
|
if self.rfilter_enabled:
|
|
self.disable_rfilter()
|
|
return True
|
|
if app.interface.msg_win_mgr.mode == \
|
|
MessageWindowMgr.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER and \
|
|
app.interface.msg_win_mgr.one_window_opened():
|
|
# let message window close the tab
|
|
return
|
|
list_of_paths = self.tree.get_selection().get_selected_rows()[1]
|
|
if not list_of_paths and not app.settings.get(
|
|
'quit_on_roster_x_button') and ((app.interface.systray_enabled and\
|
|
app.settings.get('trayicon') == 'always') or app.settings.get(
|
|
'allow_hide_roster')):
|
|
if os.name == 'nt' or app.settings.get('hide_on_roster_x_button'):
|
|
self.window.hide()
|
|
else:
|
|
self.window.iconify()
|
|
elif event.get_state() & Gdk.ModifierType.CONTROL_MASK and event.keyval == \
|
|
Gdk.KEY_i:
|
|
treeselection = self.tree.get_selection()
|
|
model, list_of_paths = treeselection.get_selected_rows()
|
|
for path in list_of_paths:
|
|
type_ = model[path][Column.TYPE]
|
|
if type_ in ('contact', 'agent'):
|
|
jid = model[path][Column.JID]
|
|
account = model[path][Column.ACCOUNT]
|
|
contact = app.contacts.get_first_contact_from_jid(account,
|
|
jid)
|
|
self.on_info(widget, contact, account)
|
|
elif event.get_state() & Gdk.ModifierType.CONTROL_MASK and event.keyval == \
|
|
Gdk.KEY_h:
|
|
if app.settings.get('one_message_window') == 'always_with_roster':
|
|
# Let MessageWindow handle this
|
|
return
|
|
treeselection = self.tree.get_selection()
|
|
model, list_of_paths = treeselection.get_selected_rows()
|
|
if len(list_of_paths) != 1:
|
|
return
|
|
path = list_of_paths[0]
|
|
type_ = model[path][Column.TYPE]
|
|
if type_ in ('contact', 'agent'):
|
|
jid = model[path][Column.JID]
|
|
account = model[path][Column.ACCOUNT]
|
|
contact = app.contacts.get_first_contact_from_jid(account,
|
|
jid)
|
|
dict_ = {'jid': GLib.Variant('s', jid),
|
|
'account': GLib.Variant('s', account)}
|
|
app.app.activate_action('browse-history',
|
|
GLib.Variant('a{sv}', dict_))
|
|
|
|
def on_roster_window_popup_menu(self, widget):
|
|
event = Gdk.Event.new(Gdk.EventType.KEY_PRESS)
|
|
self.show_treeview_menu(event)
|
|
|
|
def on_row_activated(self, widget, path):
|
|
"""
|
|
When an iter is activated (double-click or single click if gnome is set
|
|
this way)
|
|
"""
|
|
model = self.modelfilter
|
|
account = model[path][Column.ACCOUNT]
|
|
type_ = model[path][Column.TYPE]
|
|
if type_ in ('group', 'account'):
|
|
if self.tree.row_expanded(path):
|
|
self.tree.collapse_row(path)
|
|
else:
|
|
self.tree.expand_row(path, False)
|
|
return
|
|
if self.rfilter_enabled:
|
|
GObject.idle_add(self.disable_rfilter)
|
|
jid = model[path][Column.JID]
|
|
resource = None
|
|
contact = app.contacts.get_contact_with_highest_priority(account, jid)
|
|
titer = model.get_iter(path)
|
|
if contact.is_groupchat:
|
|
first_ev = app.events.get_first_event(account, jid)
|
|
if first_ev and self.open_event(account, jid, first_ev):
|
|
# We are invited to a GC
|
|
# open event cares about connecting to it
|
|
self.remove_groupchat(jid, account)
|
|
else:
|
|
self.on_groupchat_maximized(None, jid, account)
|
|
return
|
|
|
|
# else
|
|
first_ev = app.events.get_first_event(account, jid)
|
|
if not first_ev:
|
|
# look in other resources
|
|
for c in app.contacts.get_contacts(account, jid):
|
|
fjid = c.get_full_jid()
|
|
first_ev = app.events.get_first_event(account, fjid)
|
|
if first_ev:
|
|
resource = c.resource
|
|
break
|
|
if not first_ev and model.iter_has_child(titer):
|
|
child_iter = model.iter_children(titer)
|
|
while not first_ev and child_iter:
|
|
child_jid = model[child_iter][Column.JID]
|
|
first_ev = app.events.get_first_event(account, child_jid)
|
|
if first_ev:
|
|
jid = child_jid
|
|
else:
|
|
child_iter = model.iter_next(child_iter)
|
|
session = None
|
|
if first_ev:
|
|
if first_ev.type_ in ('chat', 'normal'):
|
|
session = first_ev.session
|
|
fjid = jid
|
|
if resource:
|
|
fjid += '/' + resource
|
|
if self.open_event(account, fjid, first_ev):
|
|
return
|
|
# else
|
|
contact = app.contacts.get_contact(account, jid, resource)
|
|
if not contact or isinstance(contact, list):
|
|
contact = app.contacts.get_contact_with_highest_priority(account,
|
|
jid)
|
|
if jid == app.get_jid_from_account(account):
|
|
resource = None
|
|
|
|
app.interface.on_open_chat_window(None, contact, account, \
|
|
resource=resource, session=session)
|
|
|
|
def on_roster_treeview_row_activated(self, widget, path, col=0):
|
|
"""
|
|
When an iter is double clicked: open the first event window
|
|
"""
|
|
self.on_row_activated(widget, path)
|
|
|
|
def on_roster_treeview_row_expanded(self, widget, titer, path):
|
|
"""
|
|
When a row is expanded change the icon of the arrow
|
|
"""
|
|
self._toggeling_row = True
|
|
model = widget.get_model()
|
|
child_model = model.get_model()
|
|
child_iter = model.convert_iter_to_child_iter(titer)
|
|
|
|
if self.regroup: # merged accounts
|
|
accounts = list(app.connections.keys())
|
|
else:
|
|
accounts = [model[titer][Column.ACCOUNT]]
|
|
|
|
type_ = model[titer][Column.TYPE]
|
|
if type_ == 'group':
|
|
group = model[titer][Column.JID]
|
|
child_model[child_iter][Column.IMG] = get_icon_name('opened')
|
|
if self.rfilter_enabled:
|
|
return
|
|
for account in accounts:
|
|
if group in app.groups[account]: # This account has this group
|
|
app.groups[account][group]['expand'] = True
|
|
if account + group in self.collapsed_rows:
|
|
self.collapsed_rows.remove(account + group)
|
|
for contact in app.contacts.iter_contacts(account):
|
|
jid = contact.jid
|
|
if group in contact.groups and \
|
|
app.contacts.is_big_brother(account, jid, accounts) and \
|
|
account + group + jid not in self.collapsed_rows:
|
|
titers = self._get_contact_iter(jid, account)
|
|
for titer_ in titers:
|
|
path = model.get_path(titer_)
|
|
self.tree.expand_row(path, False)
|
|
elif type_ == 'account':
|
|
account = list(accounts)[0] # There is only one cause we don't use merge
|
|
if account in self.collapsed_rows:
|
|
self.collapsed_rows.remove(account)
|
|
self.draw_account(account)
|
|
# When we expand, groups are collapsed. Restore expand state
|
|
for group in app.groups[account]:
|
|
if app.groups[account][group]['expand']:
|
|
titer = self._get_group_iter(group, account)
|
|
if titer:
|
|
path = model.get_path(titer)
|
|
self.tree.expand_row(path, False)
|
|
elif type_ == 'contact':
|
|
# Metacontact got toggled, update icon
|
|
jid = model[titer][Column.JID]
|
|
account = model[titer][Column.ACCOUNT]
|
|
contact = app.contacts.get_contact(account, jid)
|
|
for group in contact.groups:
|
|
if account + group + jid in self.collapsed_rows:
|
|
self.collapsed_rows.remove(account + group + jid)
|
|
family = app.contacts.get_metacontacts_family(account, jid)
|
|
nearby_family = \
|
|
self._get_nearby_family_and_big_brother(family, account)[0]
|
|
# Redraw all brothers to show pending events
|
|
for data in nearby_family:
|
|
self.draw_contact(data['jid'], data['account'])
|
|
|
|
self._toggeling_row = False
|
|
|
|
def on_roster_treeview_row_collapsed(self, widget, titer, path):
|
|
"""
|
|
When a row is collapsed change the icon of the arrow
|
|
"""
|
|
self._toggeling_row = True
|
|
model = widget.get_model()
|
|
child_model = model.get_model()
|
|
child_iter = model.convert_iter_to_child_iter(titer)
|
|
|
|
if self.regroup: # merged accounts
|
|
accounts = list(app.connections.keys())
|
|
else:
|
|
accounts = [model[titer][Column.ACCOUNT]]
|
|
|
|
type_ = model[titer][Column.TYPE]
|
|
if type_ == 'group':
|
|
child_model[child_iter][Column.IMG] = get_icon_name('closed')
|
|
if self.rfilter_enabled:
|
|
return
|
|
group = model[titer][Column.JID]
|
|
for account in accounts:
|
|
if group in app.groups[account]: # This account has this group
|
|
app.groups[account][group]['expand'] = False
|
|
if account + group not in self.collapsed_rows:
|
|
self.collapsed_rows.append(account + group)
|
|
elif type_ == 'account':
|
|
account = accounts[0] # There is only one cause we don't use merge
|
|
if account not in self.collapsed_rows:
|
|
self.collapsed_rows.append(account)
|
|
self.draw_account(account)
|
|
elif type_ == 'contact':
|
|
# Metacontact got toggled, update icon
|
|
jid = model[titer][Column.JID]
|
|
account = model[titer][Column.ACCOUNT]
|
|
contact = app.contacts.get_contact(account, jid)
|
|
groups = contact.groups
|
|
if not groups:
|
|
groups = [_('General')]
|
|
for group in groups:
|
|
if account + group + jid not in self.collapsed_rows:
|
|
self.collapsed_rows.append(account + group + jid)
|
|
family = app.contacts.get_metacontacts_family(account, jid)
|
|
nearby_family = \
|
|
self._get_nearby_family_and_big_brother(family, account)[0]
|
|
# Redraw all brothers to show pending events
|
|
for data in nearby_family:
|
|
self.draw_contact(data['jid'], data['account'])
|
|
|
|
self._toggeling_row = False
|
|
|
|
def on_modelfilter_row_has_child_toggled(self, model, path, titer):
|
|
"""
|
|
Called when a row has gotten the first or lost its last child row
|
|
|
|
Expand Parent if necessary.
|
|
"""
|
|
if self._toggeling_row:
|
|
# Signal is emitted when we write to our model
|
|
return
|
|
|
|
type_ = model[titer][Column.TYPE]
|
|
account = model[titer][Column.ACCOUNT]
|
|
if not account:
|
|
return
|
|
|
|
if type_ == 'contact':
|
|
child_iter = model.convert_iter_to_child_iter(titer)
|
|
if self.model.iter_has_child(child_iter):
|
|
# we are a bigbrother metacontact
|
|
# redraw us to show/hide expand icon
|
|
if self.filtering:
|
|
# Prevent endless loops
|
|
jid = model[titer][Column.JID]
|
|
GLib.idle_add(self.draw_contact, jid, account)
|
|
elif type_ == 'group':
|
|
group = model[titer][Column.JID]
|
|
GLib.idle_add(self._adjust_group_expand_collapse_state, group, account)
|
|
elif type_ == 'account':
|
|
GLib.idle_add(self._adjust_account_expand_collapse_state, account)
|
|
|
|
# Selection can change when the model is filtered
|
|
# Only write to the model when filtering is finished!
|
|
#
|
|
# FIXME: When we are filtering our custom colors are somehow lost
|
|
#
|
|
# def on_treeview_selection_changed(self, selection):
|
|
# '''Called when selection in TreeView has changed.
|
|
#
|
|
# Redraw unselected rows to make status message readable
|
|
# on all possible backgrounds.
|
|
# '''
|
|
# model, list_of_paths = selection.get_selected_rows()
|
|
# if len(self._last_selected_contact):
|
|
# # update unselected rows
|
|
# for (jid, account) in self._last_selected_contact:
|
|
# GLib.idle_add(self.draw_contact, jid,
|
|
# account)
|
|
# self._last_selected_contact = []
|
|
# if len(list_of_paths) == 0:
|
|
# return
|
|
# for path in list_of_paths:
|
|
# row = model[path]
|
|
# if row[Column.TYPE] != 'contact':
|
|
# self._last_selected_contact = []
|
|
# return
|
|
# jid = row[Column.JID]
|
|
# account = row[Column.ACCOUNT]
|
|
# self._last_selected_contact.append((jid, account))
|
|
# GLib.idle_add(self.draw_contact, jid, account, True)
|
|
|
|
|
|
def on_service_disco_menuitem_activate(self, widget, account):
|
|
server_jid = app.settings.get_account_setting(account, 'hostname')
|
|
if server_jid in app.interface.instances[account]['disco']:
|
|
app.interface.instances[account]['disco'][server_jid].\
|
|
window.present()
|
|
else:
|
|
try:
|
|
# Object will add itself to the window dict
|
|
ServiceDiscoveryWindow(account, address_entry=True)
|
|
except GajimGeneralException:
|
|
pass
|
|
|
|
def on_show_offline_contacts_action(self, action, param):
|
|
"""
|
|
When show offline option is changed: redraw the treeview
|
|
"""
|
|
action.set_state(param)
|
|
app.settings.set('showoffline', param.get_boolean())
|
|
self.refilter_shown_roster_items()
|
|
self.window.lookup_action('show-active').set_enabled(
|
|
not param.get_boolean())
|
|
|
|
def on_show_active_contacts_action(self, action, param):
|
|
"""
|
|
When show only active contact option is changed: redraw the treeview
|
|
"""
|
|
action.set_state(param)
|
|
app.settings.set('show_only_chat_and_online', param.get_boolean())
|
|
self.refilter_shown_roster_items()
|
|
self.window.lookup_action('show-offline').set_enabled(
|
|
not param.get_boolean())
|
|
|
|
def on_show_roster_action(self, action, param):
|
|
# when num controls is 0 this menuitem is hidden, but still need to
|
|
# disable keybinding
|
|
action.set_state(param)
|
|
if self.hpaned.get_child2() is not None:
|
|
self.show_roster_vbox(param.get_boolean())
|
|
|
|
def on_rfilter_entry_changed(self, widget):
|
|
""" When we update the content of the filter """
|
|
self.rfilter_string = widget.get_text().lower()
|
|
if self.rfilter_string == '':
|
|
self.disable_rfilter()
|
|
self.refilter_shown_roster_items()
|
|
# select first row
|
|
self.tree.get_selection().unselect_all()
|
|
def _func(model, path, iter_, param):
|
|
if model[iter_][Column.TYPE] == 'contact' and self.rfilter_string in \
|
|
model[iter_][Column.NAME].lower():
|
|
col = self.tree.get_column(0)
|
|
self.tree.set_cursor_on_cell(path, col, None, False)
|
|
return True
|
|
self.modelfilter.foreach(_func, None)
|
|
|
|
def on_rfilter_entry_icon_press(self, widget, icon, event):
|
|
"""
|
|
Disable the roster filtering by clicking the icon in the textEntry
|
|
"""
|
|
self.disable_rfilter()
|
|
|
|
def on_rfilter_entry_key_press_event(self, widget, event):
|
|
if event.keyval == Gdk.KEY_Escape:
|
|
self.disable_rfilter()
|
|
elif event.keyval == Gdk.KEY_Return:
|
|
self.tree.grab_focus()
|
|
self.tree.event(event)
|
|
self.disable_rfilter()
|
|
elif event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down):
|
|
self.tree.grab_focus()
|
|
self.tree.event(event)
|
|
elif event.keyval == Gdk.KEY_BackSpace:
|
|
if widget.get_text() == '':
|
|
self.disable_rfilter()
|
|
|
|
def enable_rfilter(self, search_string):
|
|
self.rfilter_entry.set_visible(True)
|
|
self.rfilter_entry.set_editable(True)
|
|
self.rfilter_entry.grab_focus()
|
|
if self.rfilter_enabled:
|
|
self.rfilter_entry.set_text(self.rfilter_entry.get_text() + \
|
|
search_string)
|
|
else:
|
|
self.rfilter_enabled = True
|
|
self.rfilter_entry.set_text(search_string)
|
|
self.tree.expand_all()
|
|
self.rfilter_entry.set_position(-1)
|
|
|
|
# If roster is hidden, let's temporarily show it. This can happen if user
|
|
# enables rfilter via keyboard shortcut.
|
|
self.show_roster_vbox(True)
|
|
|
|
def disable_rfilter(self):
|
|
self.rfilter_enabled = False
|
|
self.rfilter_entry.set_text('')
|
|
self.rfilter_entry.set_visible(False)
|
|
self.rfilter_entry.set_editable(False)
|
|
self.refilter_shown_roster_items()
|
|
self.tree.grab_focus()
|
|
self._readjust_expand_collapse_state()
|
|
|
|
# If roster was hidden before enable_rfilter was called, hide it back.
|
|
state = self.window.lookup_action('show-roster').get_state().get_boolean()
|
|
if state is False and self.hpaned.get_child2() is not None:
|
|
self.show_roster_vbox(False)
|
|
|
|
def on_roster_hpaned_notify(self, pane, gparamspec):
|
|
"""
|
|
Keep changing the width of the roster
|
|
(when a Gtk.Paned widget handle is dragged)
|
|
"""
|
|
if gparamspec and gparamspec.name == 'position':
|
|
roster_width = pane.get_child1().get_allocation().width
|
|
app.settings.set('roster_width', roster_width)
|
|
app.settings.set('roster_hpaned_position', pane.get_position())
|
|
|
|
################################################################################
|
|
### Drag and Drop handling
|
|
################################################################################
|
|
|
|
def drag_data_get_data(self, treeview, context, selection, target_id,
|
|
etime):
|
|
model, list_of_paths = self.tree.get_selection().get_selected_rows()
|
|
if len(list_of_paths) != 1:
|
|
return
|
|
path = list_of_paths[0]
|
|
data = ''
|
|
if path.get_depth() >= 2:
|
|
data = model[path][Column.JID]
|
|
selection.set_text(data, -1)
|
|
|
|
def drag_begin(self, treeview, context):
|
|
self.dragging = True
|
|
|
|
def drag_end(self, treeview, context):
|
|
self.dragging = False
|
|
|
|
def on_drop_rosterx(self, widget, account_source, c_source, account_dest,
|
|
c_dest, was_big_brother, context, etime):
|
|
type_ = 'message'
|
|
if (c_dest.show not in ('offline', 'error') and
|
|
c_dest.supports(Namespace.ROSTERX)):
|
|
type_ = 'iq'
|
|
con = app.connections[account_dest]
|
|
con.get_module('RosterItemExchange').send_contacts(
|
|
[c_source], c_dest.get_full_jid(), type_=type_)
|
|
|
|
def on_drop_in_contact(self, widget, account_source, c_source, account_dest,
|
|
c_dest, was_big_brother, context, etime):
|
|
con_source = app.connections[account_source]
|
|
con_dest = app.connections[account_dest]
|
|
if (not con_source.get_module('MetaContacts').available or
|
|
not con_dest.get_module('MetaContacts').available):
|
|
return
|
|
|
|
def merge_contacts(is_checked=None):
|
|
contacts = 0
|
|
if is_checked is not None: # dialog has been shown
|
|
if is_checked: # user does not want to be asked again
|
|
app.settings.set('confirm_metacontacts', 'no')
|
|
else:
|
|
app.settings.set('confirm_metacontacts', 'yes')
|
|
|
|
# We might have dropped on a metacontact.
|
|
# Remove it and add it again later with updated family info
|
|
dest_family = app.contacts.get_metacontacts_family(account_dest,
|
|
c_dest.jid)
|
|
if dest_family:
|
|
self._remove_metacontact_family(dest_family, account_dest)
|
|
source_family = app.contacts.get_metacontacts_family(
|
|
account_source, c_source.jid)
|
|
if dest_family == source_family:
|
|
n = contacts = len(dest_family)
|
|
for tag in source_family:
|
|
if tag['jid'] == c_source.jid:
|
|
tag['order'] = contacts
|
|
continue
|
|
if 'order' in tag:
|
|
n -= 1
|
|
tag['order'] = n
|
|
else:
|
|
self._remove_entity(c_dest, account_dest)
|
|
|
|
old_family = app.contacts.get_metacontacts_family(account_source,
|
|
c_source.jid)
|
|
old_groups = c_source.groups
|
|
|
|
# Remove old source contact(s)
|
|
if was_big_brother:
|
|
# We have got little brothers. Add them all back
|
|
self._remove_metacontact_family(old_family, account_source)
|
|
else:
|
|
# We are only a little brother. Simply remove us from our big
|
|
# brother
|
|
if self._get_contact_iter(c_source.jid, account_source):
|
|
# When we have been in the group before.
|
|
# Do not try to remove us again
|
|
self._remove_entity(c_source, account_source)
|
|
|
|
own_data = {}
|
|
own_data['jid'] = c_source.jid
|
|
own_data['account'] = account_source
|
|
# Don't touch the rest of the family
|
|
old_family = [own_data]
|
|
|
|
# Apply new tag and update contact
|
|
for data in old_family:
|
|
if account_source != data['account'] and not self.regroup:
|
|
continue
|
|
|
|
_account = data['account']
|
|
_jid = data['jid']
|
|
_contact = app.contacts.get_first_contact_from_jid(_account,
|
|
_jid)
|
|
if not _contact:
|
|
# One of the metacontacts may be not connected.
|
|
continue
|
|
|
|
_contact.groups = c_dest.groups[:]
|
|
app.contacts.add_metacontact(account_dest, c_dest.jid,
|
|
_account, _contact.jid, contacts)
|
|
con = app.connections[account_source]
|
|
con.get_module('Roster').update_contact(
|
|
_contact.jid, _contact.name, _contact.groups)
|
|
|
|
# Re-add all and update GUI
|
|
new_family = app.contacts.get_metacontacts_family(account_source,
|
|
c_source.jid)
|
|
brothers = self._add_metacontact_family(new_family, account_source)
|
|
|
|
for c, acc in brothers:
|
|
self.draw_completely(c.jid, acc)
|
|
|
|
old_groups.extend(c_dest.groups)
|
|
for g in old_groups:
|
|
self.draw_group(g, account_source)
|
|
|
|
self.draw_account(account_source)
|
|
context.finish(True, True, etime)
|
|
|
|
dest_family = app.contacts.get_metacontacts_family(account_dest,
|
|
c_dest.jid)
|
|
source_family = app.contacts.get_metacontacts_family(account_source,
|
|
c_source.jid)
|
|
confirm_metacontacts = app.settings.get('confirm_metacontacts')
|
|
if confirm_metacontacts == 'no' or dest_family == source_family:
|
|
merge_contacts()
|
|
return
|
|
pritext = _('You are about to create a metacontact')
|
|
sectext = _('Metacontacts are a way to regroup several contacts in '
|
|
'one single contact. Generally it is used when the same '
|
|
'person has several XMPP- or Transport-Accounts.')
|
|
ConfirmationCheckDialog(
|
|
_('Create Metacontact'),
|
|
pritext,
|
|
sectext,
|
|
_('_Do not ask me again'),
|
|
[DialogButton.make('Cancel'),
|
|
DialogButton.make('Accept',
|
|
text=_('_Create'),
|
|
callback=merge_contacts)]).show()
|
|
|
|
def on_drop_in_group(self, widget, account, c_source, grp_dest,
|
|
is_big_brother, context, etime, grp_source=None):
|
|
if is_big_brother:
|
|
# add whole metacontact to new group
|
|
self.add_contact_to_groups(c_source.jid, account, [grp_dest, ])
|
|
# remove afterwards so the contact is not moved to General in the
|
|
# meantime
|
|
if grp_dest != grp_source:
|
|
self.remove_contact_from_groups(c_source.jid, account,
|
|
[grp_source])
|
|
else:
|
|
# Normal contact or little brother
|
|
family = app.contacts.get_metacontacts_family(account,
|
|
c_source.jid)
|
|
if family:
|
|
# Little brother
|
|
# Remove whole family. Remove us from the family.
|
|
# Then re-add other family members.
|
|
self._remove_metacontact_family(family, account)
|
|
app.contacts.remove_metacontact(account, c_source.jid)
|
|
for data in family:
|
|
if account != data['account'] and not self.regroup:
|
|
continue
|
|
if data['jid'] == c_source.jid and\
|
|
data['account'] == account:
|
|
continue
|
|
self.add_contact(data['jid'], data['account'])
|
|
break
|
|
|
|
self.add_contact_to_groups(c_source.jid, account, [grp_dest, ])
|
|
|
|
else:
|
|
# Normal contact
|
|
self.add_contact_to_groups(c_source.jid, account, [grp_dest, ])
|
|
# remove afterwards so the contact is not moved to General in
|
|
# the meantime
|
|
if grp_dest != grp_source:
|
|
self.remove_contact_from_groups(c_source.jid, account,
|
|
[grp_source])
|
|
|
|
if context.get_actions() in (Gdk.DragAction.MOVE, Gdk.DragAction.COPY):
|
|
context.finish(True, True, etime)
|
|
|
|
def drag_drop(self, treeview, context, x, y, timestamp):
|
|
treeview.stop_emission_by_name('drag-drop')
|
|
target_list = treeview.drag_dest_get_target_list()
|
|
target = treeview.drag_dest_find_target(context, target_list)
|
|
treeview.drag_get_data(context, target, timestamp)
|
|
return True
|
|
|
|
def move_group(self, old_name, new_name, account):
|
|
for group in list(app.groups[account].keys()):
|
|
if group.startswith(old_name):
|
|
self.rename_group(group, group.replace(old_name, new_name),
|
|
account)
|
|
|
|
def drag_data_received_data(self, treeview, context, x, y, selection, info,
|
|
etime):
|
|
treeview.stop_emission_by_name('drag-data-received')
|
|
drop_info = treeview.get_dest_row_at_pos(x, y)
|
|
if not drop_info:
|
|
return
|
|
data = selection.get_data().decode()
|
|
if not data:
|
|
return # prevents tb when several entries are dragged
|
|
model = treeview.get_model()
|
|
|
|
path_dest, position = drop_info
|
|
|
|
if position == Gtk.TreeViewDropPosition.BEFORE and len(path_dest) == 2 \
|
|
and path_dest[1] == 0: # dropped before the first group
|
|
return
|
|
if position == Gtk.TreeViewDropPosition.BEFORE and len(path_dest) == 2:
|
|
# dropped before a group: we drop it in the previous group every
|
|
# time
|
|
path_dest = (path_dest[0], path_dest[1]-1)
|
|
# destination: the row something got dropped on
|
|
iter_dest = model.get_iter(path_dest)
|
|
type_dest = model[iter_dest][Column.TYPE]
|
|
jid_dest = model[iter_dest][Column.JID]
|
|
account_dest = model[iter_dest][Column.ACCOUNT]
|
|
|
|
# drop on account row in merged mode, we cannot know the desired account
|
|
if account_dest == 'all':
|
|
return
|
|
# nothing can be done, if destination account is offline
|
|
if not app.account_is_available(account_dest):
|
|
return
|
|
|
|
# A file got dropped on the roster
|
|
if info == self.TARGET_TYPE_URI_LIST:
|
|
if len(path_dest) < 3:
|
|
return
|
|
if type_dest != 'contact':
|
|
return
|
|
c_dest = app.contacts.get_contact_with_highest_priority(
|
|
account_dest, jid_dest)
|
|
if not c_dest.supports(Namespace.JINGLE_FILE_TRANSFER_5):
|
|
return
|
|
uri = data.strip()
|
|
uri_splitted = uri.split() # we may have more than one file dropped
|
|
try:
|
|
# This is always the last element in windows
|
|
uri_splitted.remove('\0')
|
|
except ValueError:
|
|
pass
|
|
nb_uri = len(uri_splitted)
|
|
# Check the URIs
|
|
bad_uris = []
|
|
for a_uri in uri_splitted:
|
|
path = helpers.get_file_path_from_dnd_dropped_uri(a_uri)
|
|
if not os.path.isfile(path):
|
|
bad_uris.append(a_uri)
|
|
if bad_uris:
|
|
ErrorDialog(_('Invalid file URI:'), '\n'.join(bad_uris))
|
|
return
|
|
def _on_send_files(account, jid, uris):
|
|
c = app.contacts.get_contact_with_highest_priority(account,
|
|
jid)
|
|
for uri in uris:
|
|
path = helpers.get_file_path_from_dnd_dropped_uri(uri)
|
|
if os.path.isfile(path): # is it file?
|
|
app.interface.instances['file_transfers'].send_file(
|
|
account, c, path)
|
|
# Popup dialog to confirm sending
|
|
text = i18n.ngettext(
|
|
'Send this file to %s:\n',
|
|
'Send these files to %s:\n',
|
|
nb_uri) % c_dest.get_shown_name()
|
|
|
|
for uri in uri_splitted:
|
|
path = helpers.get_file_path_from_dnd_dropped_uri(uri)
|
|
text += '\n' + os.path.basename(path)
|
|
ConfirmationDialog(
|
|
_('File Transfer'),
|
|
_('File Transfer'),
|
|
text,
|
|
[DialogButton.make('Cancel'),
|
|
DialogButton.make('Accept',
|
|
text=_('_Send'),
|
|
callback=_on_send_files,
|
|
args=(account_dest, jid_dest, uri_splitted))],
|
|
transient_for=self.window).show()
|
|
return
|
|
|
|
# Check if something is selected
|
|
if treeview.get_selection().count_selected_rows() == 0:
|
|
return
|
|
|
|
# a roster entry was dragged and dropped somewhere in the roster
|
|
|
|
# source: the row that was dragged
|
|
path_source = treeview.get_selection().get_selected_rows()[1][0]
|
|
iter_source = model.get_iter(path_source)
|
|
type_source = model[iter_source][Column.TYPE]
|
|
account_source = model[iter_source][Column.ACCOUNT]
|
|
|
|
if app.settings.get_account_setting(account_source, 'is_zeroconf'):
|
|
return
|
|
|
|
if type_dest == 'self_contact':
|
|
# drop on self contact row
|
|
return
|
|
|
|
if type_dest == 'groupchat':
|
|
# Drop on a minimized groupchat
|
|
if type_source != 'contact':
|
|
return
|
|
contact_jid = data
|
|
gc_control = app.get_groupchat_control(account_dest, jid_dest)
|
|
if gc_control is not None:
|
|
gc_control.invite(contact_jid)
|
|
return
|
|
|
|
if type_source == 'group':
|
|
if account_source != account_dest:
|
|
# drop on another account
|
|
return
|
|
grp_source = model[iter_source][Column.JID]
|
|
delimiter = app.connections[account_source].get_module('Delimiter').delimiter
|
|
grp_source_list = grp_source.split(delimiter)
|
|
new_grp = None
|
|
if type_dest == 'account':
|
|
new_grp = grp_source_list[-1]
|
|
elif type_dest == 'group':
|
|
grp_dest = model[iter_dest][Column.JID]
|
|
# Don't allow to drop on e.g. Groupchats group
|
|
if grp_dest in helpers.special_groups:
|
|
return
|
|
grp_dest_list = grp_dest.split(delimiter)
|
|
# Do not allow to drop on a subgroup of source group
|
|
if grp_source_list[0] != grp_dest_list[0]:
|
|
new_grp = model[iter_dest][Column.JID] + delimiter + \
|
|
grp_source_list[-1]
|
|
if new_grp:
|
|
self.move_group(grp_source, new_grp, account_source)
|
|
|
|
# Only normal contacts and group can be dragged
|
|
if type_source != 'contact':
|
|
return
|
|
|
|
# A contact was dropped
|
|
if app.settings.get_account_setting(account_dest, 'is_zeroconf'):
|
|
# drop on zeroconf account, adding not possible
|
|
return
|
|
|
|
if type_dest == 'account' and account_source == account_dest:
|
|
# drop on the account it was dragged from
|
|
return
|
|
|
|
# Get valid source group, jid and contact
|
|
it = iter_source
|
|
while model[it][Column.TYPE] == 'contact':
|
|
it = model.iter_parent(it)
|
|
grp_source = model[it][Column.JID]
|
|
if grp_source in (_('Transports'), _('Group chats')):
|
|
# a transport or a minimized groupchat was dragged
|
|
# we can add it to other accounts but not move it to another group,
|
|
# see below
|
|
return
|
|
jid_source = data
|
|
c_source = app.contacts.get_contact_with_highest_priority(
|
|
account_source, jid_source)
|
|
|
|
# Get destination group
|
|
grp_dest = None
|
|
if type_dest == 'group':
|
|
grp_dest = model[iter_dest][Column.JID]
|
|
elif type_dest in ('contact', 'agent'):
|
|
it = iter_dest
|
|
while model[it][Column.TYPE] != 'group':
|
|
it = model.iter_parent(it)
|
|
grp_dest = model[it][Column.JID]
|
|
if grp_dest in helpers.special_groups:
|
|
return
|
|
|
|
if jid_source == jid_dest:
|
|
if grp_source == grp_dest and account_source == account_dest:
|
|
# Drop on self
|
|
return
|
|
|
|
# contact drop somewhere in or on a foreign account
|
|
if (type_dest == 'account' or not self.regroup) and \
|
|
account_source != account_dest:
|
|
# add to account in specified group
|
|
AddNewContactWindow(account=account_dest, contact_jid=jid_source,
|
|
user_nick=c_source.name, group=grp_dest)
|
|
return
|
|
|
|
# we may not add contacts from special_groups
|
|
if grp_source in helpers.special_groups:
|
|
if grp_source == _('Not in contact list'):
|
|
AddNewContactWindow(
|
|
account=account_dest,
|
|
contact_jid=jid_source,
|
|
user_nick=c_source.name,
|
|
group=grp_dest)
|
|
return
|
|
return
|
|
|
|
# Is the contact we drag a meta contact?
|
|
accounts = account_source
|
|
if self.regroup:
|
|
accounts = app.contacts.get_accounts() or account_source
|
|
is_big_brother = app.contacts.is_big_brother(account_source,
|
|
jid_source, accounts)
|
|
|
|
drop_in_middle_of_meta = False
|
|
if type_dest == 'contact':
|
|
if position == Gtk.TreeViewDropPosition.BEFORE and len(path_dest) == 4:
|
|
drop_in_middle_of_meta = True
|
|
if position == Gtk.TreeViewDropPosition.AFTER and (len(path_dest) == 4 or\
|
|
self.modelfilter.iter_has_child(iter_dest)):
|
|
drop_in_middle_of_meta = True
|
|
# Contact drop on group row or between two contacts that are
|
|
# not metacontacts
|
|
if (type_dest == 'group' or position in (Gtk.TreeViewDropPosition.BEFORE,
|
|
Gtk.TreeViewDropPosition.AFTER)) and not drop_in_middle_of_meta:
|
|
self.on_drop_in_group(None, account_source, c_source, grp_dest,
|
|
is_big_brother, context, etime, grp_source)
|
|
return
|
|
|
|
# Contact drop on another contact, make meta contacts
|
|
if position == Gtk.TreeViewDropPosition.INTO_OR_AFTER or \
|
|
position == Gtk.TreeViewDropPosition.INTO_OR_BEFORE or drop_in_middle_of_meta:
|
|
c_dest = app.contacts.get_contact_with_highest_priority(
|
|
account_dest, jid_dest)
|
|
if not c_dest:
|
|
# c_dest is None if jid_dest doesn't belong to account
|
|
return
|
|
menu = Gtk.Menu()
|
|
#from and to are the names of contacts
|
|
item = Gtk.MenuItem.new_with_label(_('Send %(from)s to %(to)s') % {
|
|
'from': c_source.get_shown_name(), 'to': c_dest.get_shown_name()})
|
|
item.set_use_underline(False)
|
|
item.connect('activate', self.on_drop_rosterx, account_source,
|
|
c_source, account_dest, c_dest, is_big_brother, context, etime)
|
|
menu.append(item)
|
|
|
|
dest_family = app.contacts.get_metacontacts_family(account_dest,
|
|
c_dest.jid)
|
|
source_family = app.contacts.get_metacontacts_family(
|
|
account_source, c_source.jid)
|
|
if dest_family == source_family and dest_family:
|
|
item = Gtk.MenuItem.new_with_label(
|
|
_('Make %s first contact') % (
|
|
c_source.get_shown_name()))
|
|
item.set_use_underline(False)
|
|
else:
|
|
item = Gtk.MenuItem.new_with_label(
|
|
_('Make %(contact1)s and %(contact2)s metacontacts') % {
|
|
'contact1': c_source.get_shown_name(), 'contact2': c_dest.get_shown_name()})
|
|
item.set_use_underline(False)
|
|
|
|
item.connect('activate', self.on_drop_in_contact, account_source,
|
|
c_source, account_dest, c_dest, is_big_brother, context, etime)
|
|
|
|
menu.append(item)
|
|
|
|
menu.attach_to_widget(self.tree, None)
|
|
menu.connect('selection-done', gtkgui_helpers.destroy_widget)
|
|
menu.show_all()
|
|
menu.popup_at_pointer(None)
|
|
|
|
################################################################################
|
|
### Everything about images and icons....
|
|
### Cleanup assigned to Jim++ :-)
|
|
################################################################################
|
|
|
|
def update_icons(self):
|
|
# Update the roster
|
|
self.setup_and_draw_roster()
|
|
|
|
# Update the systray
|
|
if app.interface.systray_enabled:
|
|
app.interface.systray.set_img()
|
|
app.interface.systray.change_status(helpers.get_global_show())
|
|
|
|
for win in app.interface.msg_win_mgr.windows():
|
|
for ctrl in win.controls():
|
|
ctrl.update_ui()
|
|
win.redraw_tab(ctrl)
|
|
|
|
self._status_selector.update()
|
|
|
|
|
|
def set_account_status_icon(self, account):
|
|
child_iterA = self._get_account_iter(account, self.model)
|
|
if not child_iterA:
|
|
return
|
|
if not self.regroup:
|
|
status = helpers.get_connection_status(account)
|
|
else: # accounts merged
|
|
status = helpers.get_global_show()
|
|
self.model[child_iterA][Column.IMG] = get_icon_name(status)
|
|
|
|
################################################################################
|
|
### Style and theme related methods
|
|
################################################################################
|
|
|
|
def show_title(self):
|
|
change_title_allowed = app.settings.get('change_roster_title')
|
|
if not change_title_allowed:
|
|
return
|
|
|
|
nb_unread = 0
|
|
for account in app.connections:
|
|
# Count events in roster title only if we don't auto open them
|
|
if not helpers.allow_popup_window(account):
|
|
nb_unread += app.events.get_nb_events(['chat', 'normal',
|
|
'file-request', 'file-error', 'file-completed',
|
|
'file-request-error', 'file-send-error', 'file-stopped',
|
|
'printed_chat'], account)
|
|
|
|
|
|
if app.settings.get('one_message_window') == 'always_with_roster':
|
|
# always_with_roster mode defers to the MessageWindow
|
|
if not app.interface.msg_win_mgr.one_window_opened():
|
|
# No MessageWindow to defer to
|
|
self.window.set_title('Gajim')
|
|
set_urgency_hint(self.window, nb_unread > 0)
|
|
return
|
|
|
|
start = ''
|
|
if nb_unread > 1:
|
|
start = '[' + str(nb_unread) + '] '
|
|
elif nb_unread == 1:
|
|
start = '* '
|
|
|
|
self.window.set_title(start + 'Gajim')
|
|
set_urgency_hint(self.window, nb_unread > 0)
|
|
|
|
def _nec_chatstate_received(self, event):
|
|
if event.contact.is_gc_contact or event.contact.is_pm_contact:
|
|
return
|
|
self.draw_contact(event.contact.jid, event.account)
|
|
|
|
def _style_changed(self, *args):
|
|
self.change_roster_style(None)
|
|
|
|
def _change_style(self, model, path, titer, option):
|
|
if option is None or model[titer][Column.TYPE] == option:
|
|
# We changed style for this type of row
|
|
model[titer][Column.NAME] = model[titer][Column.NAME]
|
|
|
|
def change_roster_style(self, option):
|
|
self.model.foreach(self._change_style, option)
|
|
for win in app.interface.msg_win_mgr.windows():
|
|
win.repaint_themed_widgets()
|
|
|
|
def repaint_themed_widgets(self):
|
|
"""
|
|
Notify windows that contain themed widgets to repaint them
|
|
"""
|
|
for win in app.interface.msg_win_mgr.windows():
|
|
win.repaint_themed_widgets()
|
|
for account in app.connections:
|
|
for ctrl in list(app.interface.minimized_controls[account].values()):
|
|
ctrl.repaint_themed_widgets()
|
|
|
|
def _iconCellDataFunc(self, column, renderer, model, titer, data=None):
|
|
"""
|
|
When a row is added, set properties for icon renderer
|
|
"""
|
|
icon_name = model[titer][Column.IMG]
|
|
if ':' in icon_name:
|
|
icon_name, expanded = icon_name.split(':')
|
|
surface = get_metacontact_surface(
|
|
icon_name, expanded == 'opened', self.scale_factor)
|
|
renderer.set_property('icon_name', None)
|
|
renderer.set_property('surface', surface)
|
|
else:
|
|
renderer.set_property('surface', None)
|
|
renderer.set_property('icon_name', icon_name)
|
|
|
|
try:
|
|
type_ = model[titer][Column.TYPE]
|
|
except TypeError:
|
|
return
|
|
if type_ == 'account':
|
|
self._set_account_row_background_color(renderer)
|
|
renderer.set_property('xalign', 0)
|
|
elif type_ == 'group':
|
|
self._set_group_row_background_color(renderer)
|
|
parent_iter = model.iter_parent(titer)
|
|
if model[parent_iter][Column.TYPE] == 'group':
|
|
renderer.set_property('xalign', 0.4)
|
|
else:
|
|
renderer.set_property('xalign', 0.6)
|
|
elif type_:
|
|
# prevent type_ = None, see http://trac.gajim.org/ticket/2534
|
|
if not model[titer][Column.JID] or not model[titer][Column.ACCOUNT]:
|
|
# This can append when at the moment we add the row
|
|
return
|
|
jid = model[titer][Column.JID]
|
|
account = model[titer][Column.ACCOUNT]
|
|
self._set_contact_row_background_color(renderer, jid, account)
|
|
parent_iter = model.iter_parent(titer)
|
|
if model[parent_iter][Column.TYPE] == 'contact':
|
|
renderer.set_property('xalign', 1)
|
|
else:
|
|
renderer.set_property('xalign', 0.6)
|
|
renderer.set_property('width', 26)
|
|
|
|
def _nameCellDataFunc(self, column, renderer, model, titer, data=None):
|
|
"""
|
|
When a row is added, set properties for name renderer
|
|
"""
|
|
try:
|
|
type_ = model[titer][Column.TYPE]
|
|
except TypeError:
|
|
return
|
|
|
|
if type_ == 'account':
|
|
color = app.css_config.get_value('.gajim-account-row', StyleAttr.COLOR)
|
|
renderer.set_property('foreground', color)
|
|
desc = app.css_config.get_font('.gajim-account-row')
|
|
renderer.set_property('font-desc', desc)
|
|
renderer.set_property('xpad', 0)
|
|
renderer.set_property('width', 3)
|
|
self._set_account_row_background_color(renderer)
|
|
elif type_ == 'group':
|
|
color = app.css_config.get_value('.gajim-group-row', StyleAttr.COLOR)
|
|
renderer.set_property('foreground', color)
|
|
desc = app.css_config.get_font('.gajim-group-row')
|
|
renderer.set_property('font-desc', desc)
|
|
parent_iter = model.iter_parent(titer)
|
|
if model[parent_iter][Column.TYPE] == 'group':
|
|
renderer.set_property('xpad', 8)
|
|
else:
|
|
renderer.set_property('xpad', 4)
|
|
self._set_group_row_background_color(renderer)
|
|
elif type_:
|
|
# prevent type_ = None, see http://trac.gajim.org/ticket/2534
|
|
if not model[titer][Column.JID] or not model[titer][Column.ACCOUNT]:
|
|
# This can append when at the moment we add the row
|
|
return
|
|
jid = model[titer][Column.JID]
|
|
account = model[titer][Column.ACCOUNT]
|
|
|
|
color = None
|
|
if type_ == 'groupchat':
|
|
ctrl = app.interface.minimized_controls[account].get(jid, None)
|
|
if ctrl and ctrl.attention_flag:
|
|
color = app.css_config.get_value(
|
|
'.state_muc_directed_msg_color', StyleAttr.COLOR)
|
|
elif app.settings.get('show_chatstate_in_roster'):
|
|
chatstate = app.contacts.get_combined_chatstate(account, jid)
|
|
if chatstate not in (None, 'active'):
|
|
color = app.css_config.get_value(
|
|
'.gajim-state-%s' % chatstate, StyleAttr.COLOR)
|
|
else:
|
|
color = app.css_config.get_value(
|
|
'.gajim-contact-row', StyleAttr.COLOR)
|
|
renderer.set_property('foreground', color)
|
|
|
|
self._set_contact_row_background_color(renderer, jid, account)
|
|
desc = app.css_config.get_font('.gajim-contact-row')
|
|
renderer.set_property('font-desc', desc)
|
|
parent_iter = model.iter_parent(titer)
|
|
if model[parent_iter][Column.TYPE] == 'contact':
|
|
renderer.set_property('xpad', 16)
|
|
else:
|
|
renderer.set_property('xpad', 12)
|
|
|
|
def _fill_pep_pixbuf_renderer(self, column, renderer, model, titer,
|
|
data=None):
|
|
"""
|
|
When a row is added, draw the respective pep icon
|
|
"""
|
|
try:
|
|
type_ = model[titer][Column.TYPE]
|
|
except TypeError:
|
|
return
|
|
|
|
# allocate space for the icon only if needed
|
|
if model[titer][data] is None:
|
|
renderer.set_property('visible', False)
|
|
else:
|
|
renderer.set_property('visible', True)
|
|
|
|
if type_ == 'account':
|
|
self._set_account_row_background_color(renderer)
|
|
renderer.set_property('xalign', 1)
|
|
elif type_:
|
|
if not model[titer][Column.JID] or not model[titer][Column.ACCOUNT]:
|
|
# This can append at the moment we add the row
|
|
return
|
|
jid = model[titer][Column.JID]
|
|
account = model[titer][Column.ACCOUNT]
|
|
self._set_contact_row_background_color(renderer, jid, account)
|
|
|
|
def _fill_avatar_pixbuf_renderer(self, column, renderer, model, titer,
|
|
data=None):
|
|
"""
|
|
When a row is added, set properties for avatar renderer
|
|
"""
|
|
try:
|
|
type_ = model[titer][Column.TYPE]
|
|
except TypeError:
|
|
return
|
|
|
|
if type_ in ('group', 'account'):
|
|
renderer.set_property('visible', False)
|
|
return
|
|
|
|
image = model[titer][Column.AVATAR_IMG]
|
|
if image is not None:
|
|
surface = image.get_property('surface')
|
|
renderer.set_property('surface', surface)
|
|
# allocate space for the icon only if needed
|
|
if model[titer][Column.AVATAR_IMG] or \
|
|
app.settings.get('avatar_position_in_roster') == 'left':
|
|
renderer.set_property('visible', True)
|
|
if type_:
|
|
# prevent type_ = None, see http://trac.gajim.org/ticket/2534
|
|
if not model[titer][Column.JID] or not model[titer][Column.ACCOUNT]:
|
|
# This can append at the moment we add the row
|
|
return
|
|
jid = model[titer][Column.JID]
|
|
account = model[titer][Column.ACCOUNT]
|
|
self._set_contact_row_background_color(renderer, jid, account)
|
|
else:
|
|
renderer.set_property('visible', False)
|
|
if model[titer][Column.AVATAR_IMG] is None and \
|
|
app.settings.get('avatar_position_in_roster') != 'left':
|
|
renderer.set_property('visible', False)
|
|
|
|
renderer.set_property('width', AvatarSize.ROSTER)
|
|
renderer.set_property('xalign', 0.5)
|
|
|
|
def _fill_padlock_pixbuf_renderer(self, column, renderer, model, titer,
|
|
data=None):
|
|
"""
|
|
When a row is added, set properties for padlock renderer
|
|
"""
|
|
try:
|
|
type_ = model[titer][Column.TYPE]
|
|
except TypeError:
|
|
return
|
|
|
|
# allocate space for the icon only if needed
|
|
if type_ == 'account' and model[titer][Column.PADLOCK_PIXBUF]:
|
|
renderer.set_property('visible', True)
|
|
self._set_account_row_background_color(renderer)
|
|
renderer.set_property('xalign', 1) # align pixbuf to the right
|
|
else:
|
|
renderer.set_property('visible', False)
|
|
|
|
def _set_account_row_background_color(self, renderer):
|
|
color = app.css_config.get_value('.gajim-account-row', StyleAttr.BACKGROUND)
|
|
renderer.set_property('cell-background', color)
|
|
|
|
def _set_contact_row_background_color(self, renderer, jid, account):
|
|
if jid in app.newly_added[account]:
|
|
renderer.set_property('cell-background', app.css_config.get_value(
|
|
'.gajim-roster-connected', StyleAttr.BACKGROUND))
|
|
elif jid in app.to_be_removed[account]:
|
|
renderer.set_property('cell-background', app.css_config.get_value(
|
|
'.gajim-roster-disconnected', StyleAttr.BACKGROUND))
|
|
else:
|
|
color = app.css_config.get_value('.gajim-contact-row', StyleAttr.BACKGROUND)
|
|
renderer.set_property('cell-background', color)
|
|
|
|
def _set_group_row_background_color(self, renderer):
|
|
color = app.css_config.get_value('.gajim-group-row', 'background')
|
|
renderer.set_property('cell-background', color)
|
|
|
|
################################################################################
|
|
### Everything about building menus
|
|
### FIXME: We really need to make it simpler! 1465 lines are a few to much....
|
|
################################################################################
|
|
|
|
def build_account_menu(self, account):
|
|
# we have to create our own set of icons for the menu
|
|
# using self.jabber_status_images is poopoo
|
|
if not app.settings.get_account_setting(account, 'is_zeroconf'):
|
|
xml = get_builder('account_context_menu.ui')
|
|
account_context_menu = xml.get_object('account_context_menu')
|
|
|
|
status_menuitem = xml.get_object('status_menuitem')
|
|
add_contact_menuitem = xml.get_object('add_contact_menuitem')
|
|
service_discovery_menuitem = xml.get_object(
|
|
'service_discovery_menuitem')
|
|
execute_command_menuitem = xml.get_object(
|
|
'execute_command_menuitem')
|
|
view_server_info_menuitem = xml.get_object(
|
|
'view_server_info_menuitem')
|
|
edit_account_menuitem = xml.get_object('edit_account_menuitem')
|
|
sub_menu = Gtk.Menu()
|
|
status_menuitem.set_submenu(sub_menu)
|
|
|
|
for show in ('online', 'away', 'xa', 'dnd'):
|
|
uf_show = helpers.get_uf_show(show, use_mnemonic=True)
|
|
item = Gtk.MenuItem.new_with_mnemonic(uf_show)
|
|
sub_menu.append(item)
|
|
item.connect('activate', self.change_status, account, show)
|
|
|
|
item = Gtk.SeparatorMenuItem.new()
|
|
sub_menu.append(item)
|
|
|
|
item = Gtk.MenuItem.new_with_mnemonic(_('_Change Status Message'))
|
|
sub_menu.append(item)
|
|
item.connect('activate', self.on_change_status_message_activate,
|
|
account)
|
|
if not app.account_is_available(account):
|
|
item.set_sensitive(False)
|
|
|
|
item = Gtk.SeparatorMenuItem.new()
|
|
sub_menu.append(item)
|
|
|
|
uf_show = helpers.get_uf_show('offline', use_mnemonic=True)
|
|
item = Gtk.MenuItem.new_with_mnemonic(uf_show)
|
|
sub_menu.append(item)
|
|
item.connect('activate', self.change_status, account, 'offline')
|
|
|
|
pep_menuitem = xml.get_object('pep_menuitem')
|
|
if app.connections[account].get_module('PEP').supported:
|
|
pep_submenu = Gtk.Menu()
|
|
pep_menuitem.set_submenu(pep_submenu)
|
|
|
|
item = Gtk.CheckMenuItem(label=_('Publish Tune'))
|
|
pep_submenu.append(item)
|
|
if sys.platform in ('win32', 'darwin'):
|
|
item.set_sensitive(False)
|
|
else:
|
|
active = app.settings.get_account_setting(account,
|
|
'publish_tune')
|
|
item.set_active(active)
|
|
item.connect('toggled', self.on_publish_tune_toggled,
|
|
account)
|
|
|
|
item = Gtk.CheckMenuItem(label=_('Publish Location'))
|
|
pep_submenu.append(item)
|
|
if not app.is_installed('GEOCLUE'):
|
|
item.set_sensitive(False)
|
|
else:
|
|
active = app.settings.get_account_setting(
|
|
account, 'publish_location')
|
|
item.set_active(active)
|
|
item.connect('toggled', self.on_publish_location_toggled,
|
|
account)
|
|
|
|
else:
|
|
pep_menuitem.set_sensitive(False)
|
|
|
|
edit_account_menuitem.set_detailed_action_name(
|
|
'app.accounts::%s' % account)
|
|
if app.connections[account].roster_supported:
|
|
add_contact_menuitem.connect('activate',
|
|
self.on_add_new_contact, account)
|
|
else:
|
|
add_contact_menuitem.set_sensitive(False)
|
|
service_discovery_menuitem.connect('activate',
|
|
self.on_service_disco_menuitem_activate, account)
|
|
hostname = app.settings.get_account_setting(account, 'hostname')
|
|
contact = app.contacts.create_contact(jid=hostname,
|
|
account=account) # Fake contact
|
|
execute_command_menuitem.connect('activate',
|
|
self.on_execute_command, contact, account)
|
|
view_server_info_menuitem.connect('activate',
|
|
self.on_view_server_info, account)
|
|
|
|
# make some items insensitive if account is offline
|
|
if not app.account_is_available(account):
|
|
for widget in (add_contact_menuitem, service_discovery_menuitem,
|
|
execute_command_menuitem, view_server_info_menuitem,
|
|
pep_menuitem):
|
|
widget.set_sensitive(False)
|
|
else:
|
|
xml = get_builder('zeroconf_context_menu.ui')
|
|
account_context_menu = xml.get_object('zeroconf_context_menu')
|
|
|
|
status_menuitem = xml.get_object('status_menuitem')
|
|
zeroconf_properties_menuitem = xml.get_object(
|
|
'zeroconf_properties_menuitem')
|
|
sub_menu = Gtk.Menu()
|
|
status_menuitem.set_submenu(sub_menu)
|
|
|
|
for show in ('online', 'away', 'dnd'):
|
|
uf_show = helpers.get_uf_show(show, use_mnemonic=True)
|
|
item = Gtk.MenuItem.new_with_mnemonic(uf_show)
|
|
sub_menu.append(item)
|
|
item.connect('activate', self.change_status, account, show)
|
|
|
|
item = Gtk.SeparatorMenuItem.new()
|
|
sub_menu.append(item)
|
|
|
|
item = Gtk.MenuItem.new_with_mnemonic(_('_Change Status Message'))
|
|
sub_menu.append(item)
|
|
item.connect('activate', self.on_change_status_message_activate,
|
|
account)
|
|
if not app.account_is_available(account):
|
|
item.set_sensitive(False)
|
|
|
|
uf_show = helpers.get_uf_show('offline', use_mnemonic=True)
|
|
item = Gtk.MenuItem.new_with_mnemonic(uf_show)
|
|
sub_menu.append(item)
|
|
item.connect('activate', self.change_status, account, 'offline')
|
|
|
|
zeroconf_properties_menuitem.set_detailed_action_name(
|
|
'app.accounts::%s' % account)
|
|
|
|
return account_context_menu
|
|
|
|
def make_account_menu(self, event, titer):
|
|
"""
|
|
Make account's popup menu
|
|
"""
|
|
model = self.modelfilter
|
|
account = model[titer][Column.ACCOUNT]
|
|
|
|
if account != 'all': # not in merged mode
|
|
menu = self.build_account_menu(account)
|
|
else:
|
|
menu = Gtk.Menu()
|
|
accounts = [] # Put accounts in a list to sort them
|
|
for account in app.connections:
|
|
accounts.append(account)
|
|
accounts.sort()
|
|
for account in accounts:
|
|
label = app.get_account_label(account)
|
|
item = Gtk.MenuItem.new_with_label(label)
|
|
account_menu = self.build_account_menu(account)
|
|
item.set_submenu(account_menu)
|
|
menu.append(item)
|
|
|
|
event_button = gtkgui_helpers.get_possible_button_event(event)
|
|
|
|
menu.attach_to_widget(self.tree, None)
|
|
menu.connect('selection-done', gtkgui_helpers.destroy_widget)
|
|
menu.show_all()
|
|
menu.popup(None, None, None, None, event_button, event.time)
|
|
|
|
def make_group_menu(self, event, iters):
|
|
"""
|
|
Make group's popup menu
|
|
"""
|
|
model = self.modelfilter
|
|
groups = []
|
|
accounts = []
|
|
|
|
list_ = [] # list of (contact, account) tuples
|
|
list_online = [] # list of (contact, account) tuples
|
|
|
|
for titer in iters:
|
|
groups.append(model[titer][Column.JID])
|
|
accounts.append(model[titer][Column.ACCOUNT])
|
|
# Don't show menu if groups of more than one account are selected
|
|
if accounts[0] != model[titer][Column.ACCOUNT]:
|
|
return
|
|
account = accounts[0]
|
|
|
|
show_bookmarked = True
|
|
for jid in app.contacts.get_jid_list(account):
|
|
contact = app.contacts.get_contact_with_highest_priority(account,
|
|
jid)
|
|
for group in groups:
|
|
if group in contact.get_shown_groups():
|
|
if contact.show not in ('offline', 'error'):
|
|
list_online.append((contact, account))
|
|
# Check that all contacts support direct NUC invite
|
|
if not contact.supports(Namespace.CONFERENCE):
|
|
show_bookmarked = False
|
|
list_.append((contact, account))
|
|
menu = Gtk.Menu()
|
|
|
|
# Make special context menu if group is Groupchats
|
|
if _('Group chats') in groups:
|
|
if len(groups) == 1:
|
|
maximize_menuitem = Gtk.MenuItem.new_with_mnemonic(
|
|
_('_Maximize All'))
|
|
maximize_menuitem.connect('activate',
|
|
self.on_all_groupchat_maximized, list_)
|
|
menu.append(maximize_menuitem)
|
|
else:
|
|
return
|
|
else:
|
|
# Send Group Message
|
|
send_group_message_item = Gtk.MenuItem.new_with_mnemonic(
|
|
_('Send Group M_essage'))
|
|
|
|
send_group_message_submenu = Gtk.Menu()
|
|
send_group_message_item.set_submenu(send_group_message_submenu)
|
|
menu.append(send_group_message_item)
|
|
|
|
group_message_to_all_item = Gtk.MenuItem.new_with_label(_(
|
|
'To all users'))
|
|
send_group_message_submenu.append(group_message_to_all_item)
|
|
|
|
group_message_to_all_online_item = Gtk.MenuItem.new_with_label(
|
|
_('To all online users'))
|
|
send_group_message_submenu.append(group_message_to_all_online_item)
|
|
|
|
group_message_to_all_online_item.connect('activate',
|
|
self.on_send_single_message_menuitem_activate, account,
|
|
list_online)
|
|
group_message_to_all_item.connect('activate',
|
|
self.on_send_single_message_menuitem_activate, account, list_)
|
|
|
|
# Invite to
|
|
invite_menuitem = Gtk.MenuItem.new_with_mnemonic(
|
|
_('In_vite to'))
|
|
if _('Transports') not in groups:
|
|
gui_menu_builder.build_invite_submenu(invite_menuitem,
|
|
list_online, show_bookmarked=show_bookmarked)
|
|
menu.append(invite_menuitem)
|
|
|
|
# there is no singlemessage and custom status for zeroconf
|
|
if app.settings.get_account_setting(account, 'is_zeroconf'):
|
|
send_group_message_item.set_sensitive(False)
|
|
|
|
if not app.account_is_available(account):
|
|
send_group_message_item.set_sensitive(False)
|
|
invite_menuitem.set_sensitive(False)
|
|
|
|
special_group = False
|
|
for group in groups:
|
|
if group in helpers.special_groups:
|
|
special_group = True
|
|
break
|
|
|
|
if not special_group and len(groups) == 1:
|
|
group = groups[0]
|
|
item = Gtk.SeparatorMenuItem.new() # separator
|
|
menu.append(item)
|
|
|
|
# Rename
|
|
rename_item = Gtk.MenuItem.new_with_mnemonic(_('_Rename…'))
|
|
menu.append(rename_item)
|
|
rename_item.connect('activate', self.on_rename, 'group', group,
|
|
account)
|
|
|
|
# Remove group
|
|
remove_item = Gtk.MenuItem.new_with_mnemonic(_('Remo_ve'))
|
|
menu.append(remove_item)
|
|
remove_item.connect('activate', self.on_remove_group_item_activated,
|
|
group, account)
|
|
|
|
# unsensitive if account is not connected
|
|
if not app.account_is_available(account):
|
|
rename_item.set_sensitive(False)
|
|
|
|
# General group cannot be changed
|
|
if group == _('General'):
|
|
rename_item.set_sensitive(False)
|
|
remove_item.set_sensitive(False)
|
|
|
|
event_button = gtkgui_helpers.get_possible_button_event(event)
|
|
|
|
menu.attach_to_widget(self.tree, None)
|
|
menu.connect('selection-done', gtkgui_helpers.destroy_widget)
|
|
menu.show_all()
|
|
menu.popup(None, None, None, None, event_button, event.time)
|
|
|
|
def make_contact_menu(self, event, titer):
|
|
"""
|
|
Make contact's popup menu
|
|
"""
|
|
model = self.modelfilter
|
|
jid = model[titer][Column.JID]
|
|
account = model[titer][Column.ACCOUNT]
|
|
contact = app.contacts.get_contact_with_highest_priority(account, jid)
|
|
menu = gui_menu_builder.get_contact_menu(contact, account)
|
|
event_button = gtkgui_helpers.get_possible_button_event(event)
|
|
menu.attach_to_widget(self.tree, None)
|
|
menu.popup(None, None, None, None, event_button, event.time)
|
|
|
|
def make_multiple_contact_menu(self, event, iters):
|
|
"""
|
|
Make group's popup menu
|
|
"""
|
|
model = self.modelfilter
|
|
list_ = [] # list of (jid, account) tuples
|
|
one_account_offline = False
|
|
is_blocked = True
|
|
blocking_supported = True
|
|
for titer in iters:
|
|
jid = model[titer][Column.JID]
|
|
account = model[titer][Column.ACCOUNT]
|
|
if not app.account_is_available(account):
|
|
one_account_offline = True
|
|
|
|
con = app.connections[account]
|
|
if not con.get_module('Blocking').supported:
|
|
blocking_supported = False
|
|
contact = app.contacts.get_contact_with_highest_priority(
|
|
account, jid)
|
|
if not helpers.jid_is_blocked(account, jid):
|
|
is_blocked = False
|
|
list_.append((contact, account))
|
|
|
|
menu = Gtk.Menu()
|
|
account = None
|
|
for (contact, current_account) in list_:
|
|
# check that we use the same account for every sender
|
|
if account is not None and account != current_account:
|
|
account = None
|
|
break
|
|
account = current_account
|
|
show_bookmarked = True
|
|
for (contact, current_account) in list_:
|
|
# Check that all contacts support direct NUC invite
|
|
if not contact.supports(Namespace.CONFERENCE):
|
|
show_bookmarked = False
|
|
break
|
|
if account is not None:
|
|
send_group_message_item = Gtk.MenuItem.new_with_mnemonic(
|
|
_('Send Group M_essage'))
|
|
menu.append(send_group_message_item)
|
|
send_group_message_item.connect('activate',
|
|
self.on_send_single_message_menuitem_activate, account, list_)
|
|
|
|
# Invite to Groupchat
|
|
invite_item = Gtk.MenuItem.new_with_mnemonic(_('In_vite to'))
|
|
|
|
gui_menu_builder.build_invite_submenu(invite_item, list_,
|
|
show_bookmarked=show_bookmarked)
|
|
menu.append(invite_item)
|
|
|
|
item = Gtk.SeparatorMenuItem.new() # separator
|
|
menu.append(item)
|
|
|
|
# Manage Transport submenu
|
|
item = Gtk.MenuItem.new_with_mnemonic(_('_Manage Contacts'))
|
|
manage_contacts_submenu = Gtk.Menu()
|
|
item.set_submenu(manage_contacts_submenu)
|
|
menu.append(item)
|
|
|
|
# Edit Groups
|
|
edit_groups_item = Gtk.MenuItem.new_with_mnemonic(_('Edit _Groups…'))
|
|
manage_contacts_submenu.append(edit_groups_item)
|
|
edit_groups_item.connect('activate', self.on_edit_groups, list_)
|
|
|
|
item = Gtk.SeparatorMenuItem.new() # separator
|
|
manage_contacts_submenu.append(item)
|
|
|
|
# Block
|
|
if is_blocked and blocking_supported:
|
|
unblock_menuitem = Gtk.MenuItem.new_with_mnemonic(_('_Unblock'))
|
|
unblock_menuitem.connect('activate', self.on_unblock, list_)
|
|
manage_contacts_submenu.append(unblock_menuitem)
|
|
else:
|
|
block_menuitem = Gtk.MenuItem.new_with_mnemonic(_('_Block'))
|
|
block_menuitem.connect('activate', self.on_block, list_)
|
|
manage_contacts_submenu.append(block_menuitem)
|
|
|
|
if not blocking_supported:
|
|
block_menuitem.set_sensitive(False)
|
|
|
|
# Remove
|
|
remove_item = Gtk.MenuItem.new_with_mnemonic(_('_Remove'))
|
|
manage_contacts_submenu.append(remove_item)
|
|
remove_item.connect('activate', self.on_req_usub, list_)
|
|
# unsensitive remove if one account is not connected
|
|
if one_account_offline:
|
|
remove_item.set_sensitive(False)
|
|
|
|
event_button = gtkgui_helpers.get_possible_button_event(event)
|
|
|
|
menu.attach_to_widget(self.tree, None)
|
|
menu.connect('selection-done', gtkgui_helpers.destroy_widget)
|
|
menu.show_all()
|
|
menu.popup(None, None, None, None, event_button, event.time)
|
|
|
|
def make_transport_menu(self, event, titer):
|
|
"""
|
|
Make transport's popup menu
|
|
"""
|
|
model = self.modelfilter
|
|
jid = model[titer][Column.JID]
|
|
account = model[titer][Column.ACCOUNT]
|
|
contact = app.contacts.get_contact_with_highest_priority(account, jid)
|
|
menu = gui_menu_builder.get_transport_menu(contact, account)
|
|
event_button = gtkgui_helpers.get_possible_button_event(event)
|
|
menu.attach_to_widget(self.tree, None)
|
|
menu.popup(None, None, None, None, event_button, event.time)
|
|
|
|
def make_groupchat_menu(self, event, titer):
|
|
model = self.modelfilter
|
|
|
|
jid = model[titer][Column.JID]
|
|
account = model[titer][Column.ACCOUNT]
|
|
contact = app.contacts.get_contact_with_highest_priority(account, jid)
|
|
menu = Gtk.Menu()
|
|
|
|
if jid in app.interface.minimized_controls[account]:
|
|
maximize_menuitem = Gtk.MenuItem.new_with_mnemonic(_(
|
|
'_Maximize'))
|
|
maximize_menuitem.connect('activate', self.on_groupchat_maximized, \
|
|
jid, account)
|
|
menu.append(maximize_menuitem)
|
|
|
|
rename_menuitem = Gtk.MenuItem.new_with_mnemonic(_('Re_name'))
|
|
rename_menuitem.connect('activate',
|
|
self.on_groupchat_rename,
|
|
jid,
|
|
account)
|
|
menu.append(rename_menuitem)
|
|
|
|
disconnect_menuitem = Gtk.MenuItem.new_with_mnemonic(_(
|
|
'_Leave'))
|
|
disconnect_menuitem.connect('activate', self.on_disconnect, jid,
|
|
account)
|
|
menu.append(disconnect_menuitem)
|
|
|
|
item = Gtk.SeparatorMenuItem.new() # separator
|
|
menu.append(item)
|
|
|
|
adhoc_menuitem = Gtk.MenuItem.new_with_mnemonic(_('Execute command'))
|
|
adhoc_menuitem.connect('activate', self.on_execute_command, contact,
|
|
account)
|
|
menu.append(adhoc_menuitem)
|
|
|
|
item = Gtk.SeparatorMenuItem.new() # separator
|
|
menu.append(item)
|
|
|
|
history_menuitem = Gtk.MenuItem.new_with_mnemonic(_('_History'))
|
|
history_menuitem.set_action_name('app.browse-history')
|
|
dict_ = {'jid': GLib.Variant('s', contact.jid),
|
|
'account': GLib.Variant('s', account)}
|
|
variant = GLib.Variant('a{sv}', dict_)
|
|
history_menuitem.set_action_target_value(variant)
|
|
|
|
menu.append(history_menuitem)
|
|
|
|
event_button = gtkgui_helpers.get_possible_button_event(event)
|
|
|
|
menu.attach_to_widget(self.tree, None)
|
|
menu.connect('selection-done', gtkgui_helpers.destroy_widget)
|
|
menu.show_all()
|
|
menu.popup(None, None, None, None, event_button, event.time)
|
|
|
|
def show_appropriate_context_menu(self, event, iters):
|
|
# iters must be all of the same type
|
|
model = self.modelfilter
|
|
type_ = model[iters[0]][Column.TYPE]
|
|
for titer in iters[1:]:
|
|
if model[titer][Column.TYPE] != type_:
|
|
return
|
|
if type_ == 'group':
|
|
self.make_group_menu(event, iters)
|
|
if type_ == 'groupchat' and len(iters) == 1:
|
|
self.make_groupchat_menu(event, iters[0])
|
|
elif type_ == 'agent' and len(iters) == 1:
|
|
self.make_transport_menu(event, iters[0])
|
|
elif type_ in ('contact', 'self_contact') and len(iters) == 1:
|
|
self.make_contact_menu(event, iters[0])
|
|
elif type_ == 'contact':
|
|
self.make_multiple_contact_menu(event, iters)
|
|
elif type_ == 'account' and len(iters) == 1:
|
|
self.make_account_menu(event, iters[0])
|
|
|
|
def show_treeview_menu(self, event):
|
|
try:
|
|
model, list_of_paths = self.tree.get_selection().get_selected_rows()
|
|
except TypeError:
|
|
self.tree.get_selection().unselect_all()
|
|
return
|
|
if not list_of_paths:
|
|
# no row is selected
|
|
return
|
|
if len(list_of_paths) > 1:
|
|
iters = []
|
|
for path in list_of_paths:
|
|
iters.append(model.get_iter(path))
|
|
else:
|
|
path = list_of_paths[0]
|
|
iters = [model.get_iter(path)]
|
|
self.show_appropriate_context_menu(event, iters)
|
|
|
|
return True
|
|
|
|
def fill_column(self, col):
|
|
for rend in self.renderers_list:
|
|
col.pack_start(rend[1], rend[2])
|
|
if rend[0] != 'avatar':
|
|
col.add_attribute(rend[1], rend[3], rend[4])
|
|
col.set_cell_data_func(rend[1], rend[5], rend[6])
|
|
# set renderers properties
|
|
for renderer in self.renderers_propertys:
|
|
renderer.set_property(self.renderers_propertys[renderer][0],
|
|
self.renderers_propertys[renderer][1])
|
|
|
|
def query_tooltip(self, widget, x_pos, y_pos, _keyboard_mode, tooltip):
|
|
try:
|
|
path = widget.get_path_at_pos(x_pos, y_pos)
|
|
row = path[0]
|
|
col = path[1]
|
|
except TypeError:
|
|
self._roster_tooltip.clear_tooltip()
|
|
return False
|
|
if not row:
|
|
self._roster_tooltip.clear_tooltip()
|
|
return False
|
|
|
|
iter_ = None
|
|
try:
|
|
model = widget.get_model()
|
|
iter_ = model.get_iter(row)
|
|
except Exception:
|
|
self._roster_tooltip.clear_tooltip()
|
|
return False
|
|
|
|
typ = model[iter_][Column.TYPE]
|
|
account = model[iter_][Column.ACCOUNT]
|
|
jid = model[iter_][Column.JID]
|
|
connected_contacts = []
|
|
|
|
if typ == 'group':
|
|
if jid == _('Observers'):
|
|
widget.set_tooltip_cell(tooltip, row, col, None)
|
|
tooltip.set_text(
|
|
_('Observers can see your status, but you '
|
|
'are not allowed to see theirs'))
|
|
return True
|
|
return False
|
|
|
|
if typ in ('contact', 'self_contact'):
|
|
contacts = app.contacts.get_contacts(account, jid)
|
|
|
|
for contact in contacts:
|
|
if contact.show not in ('offline', 'error'):
|
|
connected_contacts.append(contact)
|
|
if not connected_contacts:
|
|
# no connected contacts, show the offline one
|
|
connected_contacts = contacts
|
|
elif typ == 'groupchat':
|
|
connected_contacts = app.contacts.get_contacts(account, jid)
|
|
elif typ != 'account':
|
|
return False
|
|
|
|
value, widget = self._roster_tooltip.get_tooltip(
|
|
row, connected_contacts, account, typ)
|
|
tooltip.set_custom(widget)
|
|
return value
|
|
|
|
def add_actions(self):
|
|
|
|
actions = [
|
|
('show-roster',
|
|
not self.xml.get_object('roster_vbox2').get_no_show_all(),
|
|
self.on_show_roster_action),
|
|
|
|
('show-offline',
|
|
app.settings.get('showoffline'),
|
|
self.on_show_offline_contacts_action),
|
|
|
|
('show-active',
|
|
app.settings.get('show_only_chat_and_online'),
|
|
self.on_show_active_contacts_action),
|
|
|
|
('show-transports',
|
|
app.settings.get('show_transports_group'),
|
|
self.on_show_transports_action),
|
|
]
|
|
|
|
for action in actions:
|
|
action_name, variant, func = action
|
|
act = Gio.SimpleAction.new_stateful(
|
|
action_name, None, GLib.Variant.new_boolean(variant))
|
|
act.connect('change-state', func)
|
|
self.window.add_action(act)
|
|
|
|
################################################################################
|
|
###
|
|
################################################################################
|
|
|
|
def __init__(self, application):
|
|
self.application = application
|
|
self.filtering = False
|
|
self.starting = False
|
|
self.starting_filtering = False
|
|
# Number of renderers plugins added
|
|
self.nb_ext_renderers = 0
|
|
# When we quit, remember if we already saved config once
|
|
self.save_done = False
|
|
|
|
# [icon, name, type, jid, account, editable, mood_pixbuf,
|
|
# activity_pixbuf, TUNE_ICON, LOCATION_ICON, avatar_img,
|
|
# padlock_pixbuf, visible]
|
|
self.columns = [str, str, str, str, str, str, str, str, str,
|
|
Gtk.Image, str, bool]
|
|
|
|
self.xml = get_builder('roster_window.ui')
|
|
self.window = self.xml.get_object('roster_window')
|
|
application.add_window(self.window)
|
|
self.add_actions()
|
|
self.hpaned = self.xml.get_object('roster_hpaned')
|
|
|
|
app.interface.msg_win_mgr = MessageWindowMgr(self.window, self.hpaned)
|
|
app.interface.msg_win_mgr.connect('window-delete',
|
|
self.on_message_window_delete)
|
|
|
|
self.advanced_menus = [] # We keep them to destroy them
|
|
if app.settings.get('roster_window_skip_taskbar'):
|
|
self.window.set_property('skip-taskbar-hint', True)
|
|
self.tree = self.xml.get_object('roster_treeview')
|
|
sel = self.tree.get_selection()
|
|
sel.set_mode(Gtk.SelectionMode.MULTIPLE)
|
|
# sel.connect('changed',
|
|
# self.on_treeview_selection_changed)
|
|
|
|
self._iters = {}
|
|
# for merged mode
|
|
self._iters['MERGED'] = {'account': None, 'groups': {}}
|
|
# holds a list of (jid, account) tuples
|
|
self._last_selected_contact = []
|
|
self.transports_state_images = {'16': {}, '32': {}, 'opened': {},
|
|
'closed': {}}
|
|
|
|
self.last_save_dir = None
|
|
self.editing_path = None # path of row with cell in edit mode
|
|
self.add_new_contact_handler_id = False
|
|
self.service_disco_handler_id = False
|
|
self.new_chat_menuitem_handler_id = False
|
|
self.single_message_menuitem_handler_id = False
|
|
self.profile_avatar_menuitem_handler_id = False
|
|
#FIXME: When list_accel_closures will be wrapped in pygtk
|
|
# no need of this variable
|
|
self.have_new_chat_accel = False # Is the "Ctrl+N" shown ?
|
|
self.regroup = app.settings.get('mergeaccounts')
|
|
self.clicked_path = None # Used remember on which row we clicked
|
|
if len(app.connections) < 2:
|
|
# Do not merge accounts if only one exists
|
|
self.regroup = False
|
|
resize_window(self.window,
|
|
app.settings.get('roster_width'),
|
|
app.settings.get('roster_height'))
|
|
restore_roster_position(self.window)
|
|
|
|
# Remove contact from roster when last event opened
|
|
# { (contact, account): { backend: boolean }
|
|
self.contacts_to_be_removed = {}
|
|
app.events.event_removed_subscribe(self.on_event_removed)
|
|
|
|
# when this value become 0 we quit main application. If it's more than 0
|
|
# it means we are waiting for this number of accounts to disconnect
|
|
# before quitting
|
|
self.quit_on_next_offline = -1
|
|
|
|
# groups to draw next time we draw groups.
|
|
self.groups_to_draw = {}
|
|
# accounts to draw next time we draw accounts.
|
|
self.accounts_to_draw = []
|
|
|
|
# Status selector
|
|
self._status_selector = StatusSelector()
|
|
self.xml.roster_vbox2.add(self._status_selector)
|
|
|
|
# Enable/Disable checkboxes at start
|
|
if app.settings.get('showoffline'):
|
|
self.window.lookup_action('show-active').set_enabled(False)
|
|
|
|
if app.settings.get('show_only_chat_and_online'):
|
|
self.window.lookup_action('show-offline').set_enabled(False)
|
|
|
|
if self.hpaned.get_child2() is None:
|
|
self.window.lookup_action('show-roster').set_enabled(False)
|
|
|
|
# columns
|
|
col = Gtk.TreeViewColumn()
|
|
# list of renderers with attributes / properties in the form:
|
|
# (name, renderer_object, expand?, attribute_name, attribute_value,
|
|
# cell_data_func, func_arg)
|
|
self.renderers_list = []
|
|
self.renderers_propertys = {}
|
|
|
|
renderer_text = Gtk.CellRendererText()
|
|
self.renderers_propertys[renderer_text] = ('ellipsize',
|
|
Pango.EllipsizeMode.END)
|
|
|
|
def add_avatar_renderer():
|
|
self.renderers_list.append(('avatar', Gtk.CellRendererPixbuf(),
|
|
False, None, Column.AVATAR_IMG,
|
|
self._fill_avatar_pixbuf_renderer, None))
|
|
|
|
if app.settings.get('avatar_position_in_roster') == 'left':
|
|
add_avatar_renderer()
|
|
|
|
self.renderers_list += (
|
|
('icon', Gtk.CellRendererPixbuf(), False,
|
|
'icon_name', Column.IMG, self._iconCellDataFunc, None),
|
|
|
|
('name', renderer_text, True,
|
|
'markup', Column.NAME, self._nameCellDataFunc, None),
|
|
|
|
('mood', Gtk.CellRendererPixbuf(), False,
|
|
'icon_name', Column.MOOD_PIXBUF,
|
|
self._fill_pep_pixbuf_renderer, Column.MOOD_PIXBUF),
|
|
|
|
('activity', Gtk.CellRendererPixbuf(), False,
|
|
'icon_name', Column.ACTIVITY_PIXBUF,
|
|
self._fill_pep_pixbuf_renderer, Column.ACTIVITY_PIXBUF),
|
|
|
|
('tune', Gtk.CellRendererPixbuf(), False,
|
|
'icon_name', Column.TUNE_ICON,
|
|
self._fill_pep_pixbuf_renderer, Column.TUNE_ICON),
|
|
|
|
('geoloc', Gtk.CellRendererPixbuf(), False,
|
|
'icon_name', Column.LOCATION_ICON,
|
|
self._fill_pep_pixbuf_renderer, Column.LOCATION_ICON))
|
|
|
|
if app.settings.get('avatar_position_in_roster') == 'right':
|
|
add_avatar_renderer()
|
|
|
|
self.renderers_list.append(('padlock', Gtk.CellRendererPixbuf(), False,
|
|
'icon_name', Column.PADLOCK_PIXBUF,
|
|
self._fill_padlock_pixbuf_renderer, None))
|
|
|
|
# fill and append column
|
|
self.fill_column(col)
|
|
self.tree.append_column(col)
|
|
|
|
# do not show gtk arrows workaround
|
|
col = Gtk.TreeViewColumn()
|
|
render_pixbuf = Gtk.CellRendererPixbuf()
|
|
col.pack_start(render_pixbuf, False)
|
|
self.tree.append_column(col)
|
|
col.set_visible(False)
|
|
self.tree.set_expander_column(col)
|
|
|
|
# Signals
|
|
# Drag
|
|
self.tree.enable_model_drag_source(
|
|
Gdk.ModifierType.BUTTON1_MASK,
|
|
[],
|
|
Gdk.DragAction.DEFAULT |
|
|
Gdk.DragAction.MOVE |
|
|
Gdk.DragAction.COPY)
|
|
self.tree.drag_source_add_text_targets()
|
|
|
|
# Drop
|
|
self.tree.enable_model_drag_dest([], Gdk.DragAction.DEFAULT)
|
|
self.TARGET_TYPE_URI_LIST = 80
|
|
uri_entry = Gtk.TargetEntry.new(
|
|
'text/uri-list',
|
|
Gtk.TargetFlags.OTHER_APP,
|
|
self.TARGET_TYPE_URI_LIST)
|
|
dst_targets = Gtk.TargetList.new([uri_entry])
|
|
dst_targets.add_text_targets(0)
|
|
self.tree.drag_dest_set_target_list(dst_targets)
|
|
|
|
# Connect
|
|
self.tree.connect('drag-begin', self.drag_begin)
|
|
self.tree.connect('drag-end', self.drag_end)
|
|
self.tree.connect('drag-drop', self.drag_drop)
|
|
self.tree.connect('drag-data-get', self.drag_data_get_data)
|
|
self.tree.connect('drag-data-received', self.drag_data_received_data)
|
|
self.dragging = False
|
|
self.xml.connect_signals(self)
|
|
self.combobox_callback_active = True
|
|
|
|
self.collapsed_rows = app.settings.get('collapsed_rows').split('\t')
|
|
self.tree.set_has_tooltip(True)
|
|
self._roster_tooltip = RosterTooltip()
|
|
self.tree.connect('query-tooltip', self.query_tooltip)
|
|
# Workaround: For strange reasons signal is behaving like row-changed
|
|
self._toggeling_row = False
|
|
self.setup_and_draw_roster()
|
|
|
|
if app.settings.get('show_roster_on_startup') == 'always':
|
|
self.window.show_all()
|
|
elif app.settings.get('show_roster_on_startup') == 'never':
|
|
if app.settings.get('trayicon') != 'always':
|
|
# Without trayicon, user should see the roster!
|
|
self.window.show_all()
|
|
app.settings.set('last_roster_visible', True)
|
|
else:
|
|
if app.settings.get('last_roster_visible') or \
|
|
app.settings.get('trayicon') != 'always':
|
|
self.window.show_all()
|
|
|
|
self.scale_factor = self.window.get_scale_factor()
|
|
|
|
accounts = app.settings.get_accounts()
|
|
|
|
if (not accounts or
|
|
accounts == ['Local'] and
|
|
not app.settings.get_account_setting('Local', 'active')):
|
|
# if we have no account configured or only Local account but not enabled
|
|
def _open_wizard():
|
|
open_window('AccountWizard')
|
|
|
|
# Open wizard only after roster is created, so we can make it
|
|
# transient for the roster window
|
|
GLib.idle_add(_open_wizard)
|
|
|
|
# Setting CTRL+S to be the shortcut to change status message
|
|
accel_group = Gtk.AccelGroup()
|
|
keyval, mod = Gtk.accelerator_parse('<Control>s')
|
|
accel_group.connect(keyval, mod, Gtk.AccelFlags.VISIBLE,
|
|
self.accel_group_func)
|
|
|
|
# Setting CTRL+k to focus rfilter_entry
|
|
keyval, mod = Gtk.accelerator_parse('<Control>k')
|
|
accel_group.connect(keyval, mod, Gtk.AccelFlags.VISIBLE,
|
|
self.accel_group_func)
|
|
self.window.add_accel_group(accel_group)
|
|
|
|
# Setting the search stuff
|
|
self.rfilter_entry = self.xml.get_object('rfilter_entry')
|
|
self.rfilter_string = ''
|
|
self.rfilter_enabled = False
|
|
self.rfilter_entry.connect('key-press-event',
|
|
self.on_rfilter_entry_key_press_event)
|
|
|
|
app.ged.register_event_handler('presence-received', ged.GUI1,
|
|
self._nec_presence_received)
|
|
app.ged.register_event_handler('roster-received', ged.GUI1,
|
|
self._nec_roster_received)
|
|
app.ged.register_event_handler('anonymous-auth', ged.GUI1,
|
|
self._nec_anonymous_auth)
|
|
app.ged.register_event_handler('our-show', ged.GUI2,
|
|
self._nec_our_show)
|
|
app.ged.register_event_handler('connection-type', ged.GUI1,
|
|
self._nec_connection_type)
|
|
app.ged.register_event_handler('agent-removed', ged.GUI1,
|
|
self._nec_agent_removed)
|
|
app.ged.register_event_handler('nickname-received', ged.GUI1,
|
|
self._on_nickname_received)
|
|
app.ged.register_event_handler('mood-received', ged.GUI1,
|
|
self._on_mood_received)
|
|
app.ged.register_event_handler('activity-received', ged.GUI1,
|
|
self._on_activity_received)
|
|
app.ged.register_event_handler('tune-received', ged.GUI1,
|
|
self._on_tune_received)
|
|
app.ged.register_event_handler('location-received', ged.GUI1,
|
|
self._on_location_received)
|
|
app.ged.register_event_handler('update-roster-avatar', ged.GUI1,
|
|
self._nec_update_avatar)
|
|
app.ged.register_event_handler('update-room-avatar', ged.GUI1,
|
|
self._nec_update_avatar)
|
|
app.ged.register_event_handler('muc-subject', ged.GUI1,
|
|
self._nec_muc_subject_received)
|
|
app.ged.register_event_handler('metacontacts-received', ged.GUI2,
|
|
self._nec_metacontacts_received)
|
|
app.ged.register_event_handler('signed-in', ged.GUI1,
|
|
self._nec_signed_in)
|
|
app.ged.register_event_handler('decrypted-message-received', ged.GUI2,
|
|
self._nec_decrypted_message_received)
|
|
app.ged.register_event_handler('blocking', ged.GUI1,
|
|
self._nec_blocking)
|
|
app.ged.register_event_handler('style-changed', ged.GUI1,
|
|
self._style_changed)
|
|
app.ged.register_event_handler('chatstate-received', ged.GUI1,
|
|
self._nec_chatstate_received)
|
|
app.ged.register_event_handler('muc-disco-update', ged.GUI1,
|
|
self._on_muc_disco_update)
|
|
app.ged.register_event_handler('bookmarks-received', ged.GUI2,
|
|
self._on_bookmarks_received)
|