This leads to multiple problems 1. We cant assume only items with id='current' are stored in the node which would lead to retracting 'current' but another item would become the last published and sent to users 2. Even if we have a SingletonNode retracting the only item means the Node would be empty and offline clients would not receive the last published item on coming online, because there is no item anymore Instead we always publish an empty item from now on
5932 lines
243 KiB
Python
5932 lines
243 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/>.
|
|
|
|
from gi.repository import Gtk
|
|
from gi.repository import Gdk
|
|
from gi.repository import GdkPixbuf
|
|
from gi.repository import Pango
|
|
from gi.repository import GObject
|
|
from gi.repository import GLib
|
|
from gi.repository import Gio
|
|
import os
|
|
import sys
|
|
import time
|
|
import locale
|
|
import logging
|
|
|
|
from enum import IntEnum, unique
|
|
|
|
from gajim import dialogs
|
|
from gajim import vcard
|
|
from gajim import disco
|
|
from gajim import gtkgui_helpers
|
|
from gajim import gui_menu_builder
|
|
from gajim import cell_renderer_image
|
|
from gajim import tooltips
|
|
from gajim import message_control
|
|
from gajim import adhoc_commands
|
|
from gajim.accounts_window import AccountsWindow
|
|
|
|
from gajim.gtk import JoinGroupchatWindow
|
|
from gajim.gtk import ConfirmationDialogCheck
|
|
from gajim.gtk import ConfirmationDialog
|
|
from gajim.gtk import ErrorDialog
|
|
from gajim.gtk import InputDialog
|
|
from gajim.gtk import WarningDialog
|
|
from gajim.gtk import InformationDialog
|
|
from gajim.gtk import NonModalConfirmationDialog
|
|
from gajim.gtk import SingleMessageWindow
|
|
from gajim.gtk import AddNewContactWindow
|
|
from gajim.gtk import ManagePEPServicesWindow
|
|
from gajim.gtk import ManageBookmarksWindow
|
|
from gajim.gtk import AccountCreationWizard
|
|
from gajim.gtk import ServiceRegistration
|
|
from gajim.gtk import HistoryWindow
|
|
|
|
from gajim.common import app
|
|
from gajim.common import helpers
|
|
from gajim.common import idle
|
|
from gajim.common.exceptions import GajimGeneralException
|
|
from gajim.common import i18n
|
|
from gajim.common.const import PEPEventType, AvatarSize, StyleAttr
|
|
if app.is_installed('GEOCLUE'):
|
|
from gajim.common import location_listener
|
|
from gajim.common import ged
|
|
from gajim.message_window import MessageWindowMgr
|
|
from nbxmpp.protocol import NS_FILE, NS_ROSTERX, NS_CONFERENCE
|
|
|
|
|
|
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 nickame
|
|
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_PIXBUF = 7
|
|
LOCATION_PIXBUF = 8
|
|
AVATAR_IMG = 9 # avatar_sha
|
|
PADLOCK_PIXBUF = 10 # use for account row only
|
|
VISIBLE = 11
|
|
|
|
empty_pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, 1, 1)
|
|
empty_pixbuf.fill(0xffffff00)
|
|
|
|
|
|
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
|
|
|
|
@staticmethod
|
|
def _status_cell_data_func(cell_layout, cell, tree_model, iter_):
|
|
if isinstance(cell, Gtk.CellRendererPixbuf):
|
|
icon_name = tree_model[iter_][1]
|
|
if icon_name is None:
|
|
return
|
|
if tree_model[iter_][2] == 'status':
|
|
cell.set_property('icon_name', icon_name)
|
|
else:
|
|
iconset_name = gtkgui_helpers.get_iconset_name_for(icon_name)
|
|
cell.set_property('icon_name', iconset_name)
|
|
else:
|
|
show = tree_model[iter_][0]
|
|
id_ = tree_model[iter_][2]
|
|
if id_ not in ('status', 'desync'):
|
|
show = helpers.get_uf_show(show)
|
|
cell.set_property('text', show)
|
|
|
|
|
|
|
|
#############################################################################
|
|
### 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, [
|
|
app.interface.jabber_state_images['16'][show],
|
|
_('Merged accounts'), 'account', '', 'all', None, None, None,
|
|
None, None, None, True] + [None] * self.nb_ext_renderers)
|
|
self._iters['MERGED']['account'] = it
|
|
else:
|
|
show = app.SHOW_LIST[app.connections[account].connected]
|
|
our_jid = app.get_jid_from_account(account)
|
|
|
|
tls_pixbuf = None
|
|
if app.account_is_securely_connected(account):
|
|
tls_pixbuf = 'changes-prevent'
|
|
|
|
it = self.model.append(None, [
|
|
app.interface.jabber_state_images['16'][show],
|
|
GLib.markup_escape_text(account), 'account', our_jid,
|
|
account, None, None, None, None, None, tls_pixbuf, 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,
|
|
[app.interface.jabber_state_images['16']['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)
|
|
assert parent_iters, 'Big brother is not yet in roster!'
|
|
|
|
# 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}
|
|
|
|
assert added_iters, '%s has not been added to roster!' % contact.jid
|
|
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)
|
|
assert iters, '%s shall be removed but is not in roster' % contact.jid
|
|
|
|
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:
|
|
assert self.model[i][Column.JID] == contact.jid and \
|
|
self.model[i][Column.ACCOUNT] == account, \
|
|
"Invalidated iters of %s" % contact.jid
|
|
|
|
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)
|
|
|
|
assert not self._get_contact_iter(big_brother_jid,
|
|
big_brother_account, big_brother_contact, self.model), \
|
|
'Big brother %s already in roster\n Family: %s' \
|
|
% (big_brother_jid, family)
|
|
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
|
|
|
|
assert not self._get_contact_iter(_jid, _account,
|
|
_contact, self.model), \
|
|
"%s already in roster.\n Family: %s" % (_jid, nearby_family)
|
|
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
|
|
assert iters, '%s shall be removed but is not in roster \
|
|
\n Family: %s' % (_jid, family)
|
|
|
|
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
|
|
old_big_jid = _jid
|
|
continue
|
|
|
|
ok = self._remove_entity(_contact, _account)
|
|
assert ok, '%s was not removed' % _jid
|
|
assert not self._get_contact_iter(_jid, _account, _contact,
|
|
self.model), '%s is removed but still in roster' % _jid
|
|
|
|
if not family_in_roster:
|
|
return False
|
|
|
|
assert old_big_jid, 'No Big Brother in nearby family %s (Family: %s)' %\
|
|
(nearby_family, family)
|
|
iters = self._get_contact_iter(old_big_jid, old_big_account,
|
|
old_big_contact, self.model)
|
|
assert iters, 'Old Big Brother %s is not in roster anymore' % \
|
|
old_big_jid
|
|
assert not self.model.iter_children(iters[0]), \
|
|
'Old Big Brother %s still has children' % old_big_jid
|
|
|
|
ok = self._remove_entity(old_big_contact, old_big_account)
|
|
assert ok, "Old Big Brother %s not removed" % old_big_jid
|
|
assert not self._get_contact_iter(old_big_jid, old_big_account,
|
|
old_big_contact, self.model), \
|
|
'Old Big Brother %s is removed but still in roster' % old_big_jid
|
|
|
|
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)
|
|
|
|
assert not self._get_contact_iter(jid, account, contact,
|
|
self.model), 'Self contact %s already in roster' % 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):
|
|
show_self_contact = app.config.get('show_self_contact')
|
|
if show_self_contact == 'never':
|
|
return
|
|
if (contact.resource != app.connections[account].server_resource \
|
|
and show_self_contact == 'when_other_resource') or \
|
|
show_self_contact == 'always':
|
|
return self._add_self_contact(account)
|
|
return
|
|
|
|
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 Roster')]
|
|
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_connected(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
|
|
else:
|
|
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].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:
|
|
app.connections[account].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:
|
|
app.connections[account].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=''):
|
|
keyID = ''
|
|
attached_keys = app.config.get_per('accounts', account,
|
|
'attached_gpg_keys').split()
|
|
if jid in attached_keys:
|
|
keyID = attached_keys[attached_keys.index(jid) + 1]
|
|
contact = app.contacts.create_not_in_roster_contact(jid=jid,
|
|
account=account, resource=resource, name=nick, keyID=keyID)
|
|
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:
|
|
assert False, 'Account iter of %s could not be found.' % account
|
|
return
|
|
|
|
num_of_accounts = app.get_number_of_connected_accounts()
|
|
num_of_secured = app.get_number_of_securely_connected_accounts()
|
|
|
|
tls_pixbuf = None
|
|
if app.account_is_securely_connected(account) and not self.regroup or\
|
|
self.regroup and num_of_secured and num_of_secured == num_of_accounts:
|
|
tls_pixbuf = 'changes-prevent'
|
|
self.model[child_iter][Column.PADLOCK_PIXBUF] = tls_pixbuf
|
|
|
|
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_connected(account) or (self.regroup and \
|
|
app.get_number_of_connected_accounts())) and app.config.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)
|
|
|
|
pep_dict = app.connections[account].pep
|
|
if app.config.get('show_mood_in_roster') and 'mood' in pep_dict:
|
|
self.model[child_iter][Column.MOOD_PIXBUF] = \
|
|
gtkgui_helpers.get_pep_as_pixbuf(pep_dict['mood'])
|
|
else:
|
|
self.model[child_iter][Column.MOOD_PIXBUF] = empty_pixbuf
|
|
|
|
if app.config.get('show_activity_in_roster') and 'activity' in \
|
|
pep_dict:
|
|
self.model[child_iter][Column.ACTIVITY_PIXBUF] = \
|
|
gtkgui_helpers.get_pep_as_pixbuf(pep_dict['activity'])
|
|
else:
|
|
self.model[child_iter][Column.ACTIVITY_PIXBUF] = empty_pixbuf
|
|
|
|
if app.config.get('show_tunes_in_roster') and 'tune' in pep_dict:
|
|
self.model[child_iter][Column.TUNE_PIXBUF] = \
|
|
gtkgui_helpers.get_pep_as_pixbuf(pep_dict['tune'])
|
|
else:
|
|
self.model[child_iter][Column.TUNE_PIXBUF] = empty_pixbuf
|
|
|
|
if app.config.get('show_location_in_roster') and 'geoloc' in \
|
|
pep_dict:
|
|
self.model[child_iter][Column.LOCATION_PIXBUF] = \
|
|
gtkgui_helpers.get_pep_as_pixbuf(pep_dict['geoloc'])
|
|
else:
|
|
self.model[child_iter][Column.LOCATION_PIXBUF] = empty_pixbuf
|
|
|
|
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 helpers.group_is_blocked(account, group):
|
|
text = '<span strikethrough="true">%s</span>' % text
|
|
if app.config.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 = False
|
|
if helpers.jid_is_blocked(account, jid):
|
|
strike = True
|
|
else:
|
|
for group in contact.get_shown_groups():
|
|
if helpers.group_is_blocked(account, group):
|
|
strike = True
|
|
break
|
|
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 contact.status and app.config.get('show_status_msgs_in_roster'):
|
|
status = contact.status.strip()
|
|
if status != '':
|
|
status = helpers.reduce_chars_newlines(status,
|
|
max_lines = 1)
|
|
# escape markup entities and make them small
|
|
|
|
# italic
|
|
name += ('\n<span size="small" style="italic" '
|
|
'alpha="70%">{}</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):
|
|
state_images = self.get_appropriate_state_images(
|
|
jid, size = 'opened',
|
|
icon_name = icon_name)
|
|
else:
|
|
state_images = self.get_appropriate_state_images(
|
|
jid, size = 'closed',
|
|
icon_name = icon_name)
|
|
|
|
# Expand/collapse icon might differ per iter
|
|
# (group)
|
|
img = state_images[icon_name]
|
|
self.model[child_iter][Column.IMG] = img
|
|
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
|
|
state_images = self.get_appropriate_state_images(jid,
|
|
icon_name = icon_name)
|
|
|
|
visible = self.contact_is_visible(contact, account)
|
|
# All iters have the same icon (no expand/collapse)
|
|
img = state_images[icon_name]
|
|
for child_iter in child_iters:
|
|
self.model[child_iter][Column.IMG] = img
|
|
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 == 'mood':
|
|
return app.config.get('show_mood_in_roster')
|
|
elif pep_type == 'activity':
|
|
return app.config.get('show_activity_in_roster')
|
|
elif pep_type == 'tune':
|
|
return app.config.get('show_tunes_in_roster')
|
|
elif pep_type == 'geoloc':
|
|
return app.config.get('show_location_in_roster')
|
|
else:
|
|
return False
|
|
|
|
def draw_all_pep_types(self, jid, account, contact=None):
|
|
for pep_type in self._pep_type_to_model_column:
|
|
self.draw_pep(jid, account, pep_type, contact=contact)
|
|
|
|
def draw_pep(self, jid, account, pep_type, contact=None):
|
|
if pep_type not in self._pep_type_to_model_column:
|
|
return
|
|
if not self._is_pep_shown_in_roster(pep_type):
|
|
return
|
|
|
|
model_column = self._pep_type_to_model_column[pep_type]
|
|
iters = self._get_contact_iter(jid, account, model=self.model)
|
|
if not iters:
|
|
return
|
|
if not contact:
|
|
contact = app.contacts.get_contact(account, jid)
|
|
if pep_type in contact.pep:
|
|
pixbuf = gtkgui_helpers.get_pep_as_pixbuf(contact.pep[pep_type])
|
|
else:
|
|
pixbuf = empty_pixbuf
|
|
for child_iter in iters:
|
|
self.model[child_iter][model_column] = pixbuf
|
|
|
|
def _get_avatar_image(self, account, jid):
|
|
if not app.config.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.config.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 readded)
|
|
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 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.config.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.config.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.config.get('show_only_chat_and_online') and contact.show in (
|
|
'away', 'xa', 'busy'):
|
|
return False
|
|
if _('Transports') in contact.get_shown_groups():
|
|
return app.config.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 Roster'):
|
|
return 1
|
|
if name2 == _('Not in Roster'):
|
|
return -1
|
|
if name1 == _('Groupchats'):
|
|
return 1
|
|
if name2 == _('Groupchats'):
|
|
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.config.get('sort_by_show_in_roster'):
|
|
cshow = {'chat':0, 'online': 1, 'away': 2, 'xa': 3, 'dnd': 4,
|
|
'invisible': 5, '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
|
|
elif 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.logger.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.logger.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.logger.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] = {}
|
|
if app.config.get('show_self_contact') == 'always':
|
|
self_jid = app.get_jid_from_account(account)
|
|
if app.connections[account].server_resource:
|
|
self_jid += '/' + app.connections[account].server_resource
|
|
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
|
|
|
|
keyID = ''
|
|
attached_keys = app.config.get_per('accounts', account,
|
|
'attached_gpg_keys').split()
|
|
if jid in attached_keys:
|
|
keyID = attached_keys[attached_keys.index(jid) + 1]
|
|
|
|
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, keyID=keyID)
|
|
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.logger.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,
|
|
session=event.session, form_node=event.form_node)
|
|
app.events.remove_events(account, jid, event)
|
|
return True
|
|
elif 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
|
|
elif 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
|
|
elif 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
|
|
elif event.type_ == 'file-hash-error':
|
|
ft.show_hash_error(jid, event.file_props, account)
|
|
app.events.remove_events(account, jid, event)
|
|
return True
|
|
elif event.type_ == 'file-completed':
|
|
ft.show_completed(jid, event.file_props)
|
|
app.events.remove_events(account, jid, event)
|
|
return True
|
|
elif event.type_ == 'gc-invitation':
|
|
dialogs.InvitationReceivedDialog(account, event.room_jid,
|
|
event.jid_from, event.password, event.reason,
|
|
is_continued=event.is_continued)
|
|
app.events.remove_events(account, jid, event)
|
|
return True
|
|
elif event.type_ == 'subscription_request':
|
|
dialogs.SubscriptionRequestWindow(jid, event.text, account,
|
|
event.nick)
|
|
app.events.remove_events(account, jid, event)
|
|
return True
|
|
elif event.type_ == 'unsubscribed':
|
|
app.interface.show_unsubscribed_dialog(account, event.contact)
|
|
app.events.remove_events(account, jid, event)
|
|
return True
|
|
elif event.type_ == 'jingle-incoming':
|
|
dialogs.VoIPCallReceivedDialog(account, event.peerjid, event.sid,
|
|
event.content_types)
|
|
app.events.remove_events(account, jid, 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:
|
|
keyID = ''
|
|
attached_keys = app.config.get_per('accounts', account,
|
|
'attached_gpg_keys').split()
|
|
if jid in attached_keys:
|
|
keyID = attached_keys[attached_keys.index(jid) + 1]
|
|
contact = app.contacts.create_contact(jid=jid, account=account,
|
|
name=nickname, groups=groups_list, show='requested', status='',
|
|
ask='none', sub='subscribe', keyID=keyID)
|
|
app.contacts.add_contact(account, contact)
|
|
else:
|
|
if not _('Not in Roster') in contact.get_shown_groups():
|
|
InformationDialog(_('Subscription request has been '
|
|
'sent'), _('If "%s" accepts this request you will know his '
|
|
'or her 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] = \
|
|
app.interface.jabber_state_images['16'][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, auto=False, to=None):
|
|
if status != 'offline':
|
|
if to is None:
|
|
app.config.set_per('accounts', account, 'last_status', status)
|
|
app.config.set_per('accounts', account, 'last_status_msg',
|
|
helpers.to_one_line(txt))
|
|
if app.connections[account].connected < 2:
|
|
self.set_connecting_state(account)
|
|
|
|
keyid = app.config.get_per('accounts', account, 'keyid')
|
|
if keyid and not app.connections[account].gpg:
|
|
WarningDialog(_('OpenPGP is not usable'),
|
|
_('Gajim needs python-gnupg >= 0.3.8\n'
|
|
'Beware there is an incompatible Python package called gnupg.\n'
|
|
'You will be connected to %s without OpenPGP.') % account)
|
|
|
|
self.send_status_continue(account, status, txt, auto, to)
|
|
|
|
def send_pep(self, account, pep_dict):
|
|
connection = app.connections[account]
|
|
|
|
if 'activity' in pep_dict:
|
|
activity = pep_dict['activity']
|
|
subactivity = pep_dict.get('subactivity', None)
|
|
activity_text = pep_dict.get('activity_text', None)
|
|
connection.get_module('UserActivity').send(
|
|
(activity, subactivity, activity_text))
|
|
else:
|
|
connection.get_module('UserActivity').send(None)
|
|
|
|
if 'mood' in pep_dict:
|
|
mood = pep_dict['mood']
|
|
mood_text = pep_dict.get('mood_text', None)
|
|
connection.get_module('UserMood').send((mood, mood_text))
|
|
else:
|
|
connection.get_module('UserMood').send(None)
|
|
|
|
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 send_status_continue(self, account, status, txt, auto, to):
|
|
if app.account_is_connected(account) and not to:
|
|
if status == 'online' and not idle.Monitor.is_unknown():
|
|
app.sleeper_state[account] = 'online'
|
|
elif app.sleeper_state[account] not in ('autoaway', 'autoxa') or \
|
|
status == 'offline':
|
|
app.sleeper_state[account] = 'off'
|
|
|
|
if to:
|
|
app.connections[account].send_custom_status(status, txt, to)
|
|
else:
|
|
if status in ('invisible', 'offline'):
|
|
self.delete_pep(app.get_jid_from_account(account), account)
|
|
was_invisible = app.connections[account].connected == \
|
|
app.SHOW_LIST.index('invisible')
|
|
app.connections[account].change_status(status, txt, auto)
|
|
|
|
if account in app.interface.status_sent_to_users:
|
|
app.interface.status_sent_to_users[account] = {}
|
|
if account in app.interface.status_sent_to_groups:
|
|
app.interface.status_sent_to_groups[account] = {}
|
|
for gc_control in app.interface.msg_win_mgr.get_controls(
|
|
message_control.TYPE_GC) + \
|
|
list(app.interface.minimized_controls[account].values()):
|
|
if gc_control.account == account:
|
|
if app.gc_connected[account][gc_control.room_jid]:
|
|
app.connections[account].send_gc_status(
|
|
gc_control.nick, gc_control.room_jid, status, txt,
|
|
auto=auto)
|
|
if was_invisible and status != 'offline':
|
|
# We come back from invisible, join bookmarks
|
|
con = app.connections[account]
|
|
con.get_module('Bookmarks').auto_join_bookmarks()
|
|
|
|
|
|
def chg_contact_status(self, contact, show, status, account):
|
|
"""
|
|
When a contact changes their status
|
|
"""
|
|
contact_instances = app.contacts.get_contacts(account, contact.jid)
|
|
contact.show = show
|
|
contact.status = status
|
|
# 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'):
|
|
if app.config.get('show_self_contact') != 'never':
|
|
# SelfContact went offline. Remove him when last pending
|
|
# message was read
|
|
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 ctrl.type_id != message_control.TYPE_GC:
|
|
ctrl.contact = app.contacts.get_contact_with_highest_priority(
|
|
account, contact.jid)
|
|
ctrl.update_status_display(name, uf_show, status)
|
|
|
|
if contact.resource:
|
|
ctrl = app.interface.msg_win_mgr.get_control(fjid, account)
|
|
if ctrl:
|
|
ctrl.update_status_display(name, uf_show, status)
|
|
|
|
# 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)
|
|
if app.config.get('show_self_contact') == 'always':
|
|
self_resource = app.connections[account].server_resource
|
|
self_contact = app.contacts.get_contact(account,
|
|
app.get_jid_from_account(account), resource=self_resource)
|
|
if self_contact:
|
|
status = app.connections[account].status
|
|
self.chg_contact_status(self_contact, show, status, 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
|
|
if account in app.con_types:
|
|
app.con_types[account] = 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)
|
|
self.update_status_combobox()
|
|
|
|
def get_status_message(self, show, on_response, show_pep=True,
|
|
always_ask=False):
|
|
"""
|
|
Get the status message by:
|
|
|
|
1/ looking in default status message
|
|
2/ asking to user if needed depending on ask_on(ff)line_status and
|
|
always_ask
|
|
show_pep can be False to hide pep things from status message or True
|
|
"""
|
|
empty_pep = {'activity': '', 'subactivity': '', 'activity_text': '',
|
|
'mood': '', 'mood_text': ''}
|
|
if show in app.config.get_per('defaultstatusmsg'):
|
|
if app.config.get_per('defaultstatusmsg', show, 'enabled'):
|
|
msg = app.config.get_per('defaultstatusmsg', show, 'message')
|
|
msg = helpers.from_one_line(msg)
|
|
on_response(msg, empty_pep)
|
|
return
|
|
if not always_ask and ((show == 'online' and not app.config.get(
|
|
'ask_online_status')) or (show in ('offline', 'invisible') and not \
|
|
app.config.get('ask_offline_status'))):
|
|
on_response('', empty_pep)
|
|
return
|
|
|
|
dlg = dialogs.ChangeStatusMessageDialog(on_response, show, show_pep)
|
|
dlg.dialog.present() # show it on current workspace
|
|
|
|
def change_status(self, widget, account, status):
|
|
def change(account, status):
|
|
def on_response(message, pep_dict):
|
|
if message is None:
|
|
# user pressed Cancel to change status message dialog
|
|
return
|
|
self.send_status(account, status, message)
|
|
self.send_pep(account, pep_dict)
|
|
self.get_status_message(status, on_response)
|
|
|
|
if status == 'invisible' and self.connected_rooms(account):
|
|
ConfirmationDialog(
|
|
_('You are participating in one or more group chats'),
|
|
_('Changing your status to invisible will result in '
|
|
'disconnection from those group chats. Are you sure you want '
|
|
'to go invisible?'), on_response_ok = (change, account, status))
|
|
else:
|
|
change(account, status)
|
|
|
|
def update_status_combobox(self):
|
|
# table to change index in connection.connected to index in combobox
|
|
table = {'offline':9, 'connecting':9, 'online':0, 'chat':1, 'away':2,
|
|
'xa':3, 'dnd':4, 'invisible':5}
|
|
|
|
liststore = self.status_combobox.get_model()
|
|
# we check if there are more options in the combobox that it should
|
|
# if yes, we remove the first ones
|
|
while len(liststore) > len(table)+2:
|
|
titer = liststore.get_iter_first()
|
|
liststore.remove(titer)
|
|
|
|
show = helpers.get_global_show()
|
|
# temporarily block signal in order not to send status that we show
|
|
# in the combobox
|
|
self.combobox_callback_active = False
|
|
if helpers.statuses_unified():
|
|
self.status_combobox.set_active(table[show])
|
|
else:
|
|
uf_show = helpers.get_uf_show(show)
|
|
liststore.prepend(['SEPARATOR', None, '', True])
|
|
status_combobox_text = uf_show + ' (' + _("desync'ed") + ')'
|
|
liststore.prepend(
|
|
[status_combobox_text, show, 'desync', False])
|
|
self.status_combobox.set_active(0)
|
|
self.combobox_callback_active = True
|
|
if app.interface.systray_enabled:
|
|
app.interface.systray.change_status(show)
|
|
|
|
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.config.get('one_message_window') == 'always_with_roster':
|
|
self.show_roster_vbox(True)
|
|
gtkgui_helpers.resize_window(self.window,
|
|
app.config.get('roster_width'),
|
|
app.config.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.config.get('quit_on_roster_x_button') and (
|
|
(app.interface.systray_enabled and app.config.get('trayicon') != \
|
|
'on_event') or app.config.get('allow_hide_roster')):
|
|
if app.config.get('save-roster-position'):
|
|
x, y = self.window.get_position()
|
|
log.debug('Save roster position (get_position): %s %s', x, y)
|
|
app.config.set('roster_x-position', x)
|
|
app.config.set('roster_y-position', y)
|
|
if os.name == 'nt' or app.config.get('hide_on_roster_x_button'):
|
|
self.window.hide()
|
|
else:
|
|
self.window.iconify()
|
|
elif app.config.get('quit_on_roster_x_button'):
|
|
self.on_quit_request()
|
|
else:
|
|
def on_ok(checked):
|
|
if checked:
|
|
app.config.set('quit_on_roster_x_button', True)
|
|
self.on_quit_request()
|
|
ConfirmationDialogCheck(_('Really quit Gajim?'),
|
|
_('Are you sure you want to quit Gajim?'),
|
|
_('Always close Gajim'), on_response_ok=on_ok)
|
|
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:
|
|
if app.config.get('save-roster-position'):
|
|
x, y = self.window.get_window().get_root_origin()
|
|
log.debug('Save roster position (get_root_origin): %s %s', x, y)
|
|
app.config.set('roster_x-position', x)
|
|
app.config.set('roster_y-position', y)
|
|
width, height = self.window.get_size()
|
|
app.config.set('roster_width', width)
|
|
app.config.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.config.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.config.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.connections[acct].connected:
|
|
get_msg = True
|
|
break
|
|
|
|
def on_continue3(message, pep_dict):
|
|
self.quit_on_next_offline = 0
|
|
accounts_to_disconnect = []
|
|
for acct in accounts:
|
|
if app.connections[acct].connected > 1:
|
|
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)
|
|
self.send_pep(acct, pep_dict)
|
|
|
|
def on_continue2(message, pep_dict):
|
|
# check if there is an active file transfer
|
|
from gajim.common.protocol.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(_('You have running file transfers'),
|
|
_('If you quit now, the file(s) being transferred will '
|
|
'be stopped. Do you still want to quit?'),
|
|
on_response_ok=(on_continue3, message, pep_dict))
|
|
return
|
|
on_continue3(message, pep_dict)
|
|
|
|
def on_continue(message, pep_dict):
|
|
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()
|
|
if not app.config.get('notify_on_all_muc_messages'):
|
|
unread_not_to_notify = app.events.get_nb_events(
|
|
['printed_gc_msg'])
|
|
unread -= unread_not_to_notify
|
|
|
|
# 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(_('You have unread messages'),
|
|
_('Messages will only be available for reading them later '
|
|
'if you have history enabled and contact is in your '
|
|
'roster.'), on_response_ok=(on_continue2,
|
|
message, pep_dict))
|
|
return
|
|
on_continue2(message, pep_dict)
|
|
|
|
if get_msg:
|
|
self.get_status_message('offline', on_continue, show_pep=False)
|
|
else:
|
|
on_continue('', None)
|
|
|
|
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.connected > 1:
|
|
GLib.timeout_add_seconds(5, self.remove_to_be_removed,
|
|
jid, account)
|
|
|
|
if obj.need_redraw:
|
|
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 and obj.need_redraw:
|
|
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_gc_presence_received(self, obj):
|
|
account = obj.conn.name
|
|
if obj.room_jid in app.interface.minimized_controls[account]:
|
|
gc_ctrl = app.interface.minimized_controls[account][obj.room_jid]
|
|
else:
|
|
return
|
|
|
|
if obj.nick == gc_ctrl.nick:
|
|
contact = app.contacts.get_contact_with_highest_priority(
|
|
account, obj.room_jid)
|
|
if contact:
|
|
contact.show = obj.show
|
|
self.draw_contact(obj.room_jid, account)
|
|
self.draw_group(_('Groupchats'), account)
|
|
|
|
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
|
|
if app.config.get('show_self_contact') == 'always':
|
|
account = obj.conn.name
|
|
self_jid = app.get_jid_from_account(account)
|
|
if self_jid not in app.contacts.get_jid_list(account):
|
|
resource = ''
|
|
if app.connections[account].server_resource:
|
|
resource = app.connections[account].server_resource
|
|
sha = app.config.get_per('accounts', 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', resource=resource, avatar_sha=sha)
|
|
app.contacts.add_contact(account, contact)
|
|
self.add_contact(self_jid, account)
|
|
if app.config.get('remember_opened_chat_controls'):
|
|
account = obj.conn.name
|
|
controls = app.config.get_per(
|
|
'accounts', 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.config.set_per(
|
|
'accounts', 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, obj):
|
|
model = self.status_combobox.get_model()
|
|
iter_ = model.get_iter_from_string('7')
|
|
if obj.show == 'offline':
|
|
# sensitivity for this menuitem
|
|
if app.get_number_of_connected_accounts() == 0:
|
|
model[iter_][3] = False
|
|
self.application.set_account_actions_state(obj.conn.name)
|
|
else:
|
|
# sensitivity for this menuitem
|
|
model[iter_][3] = True
|
|
self.on_status_changed(obj.conn.name, obj.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 _nec_pep_received(self, obj):
|
|
if obj.user_pep.type_ not in (PEPEventType.ACTIVITY,
|
|
PEPEventType.TUNE,
|
|
PEPEventType.MOOD,
|
|
PEPEventType.LOCATION,
|
|
PEPEventType.NICKNAME):
|
|
return
|
|
|
|
if obj.jid == app.get_jid_from_account(obj.conn.name):
|
|
self.draw_account(obj.conn.name)
|
|
|
|
if obj.pep_type == 'nick':
|
|
self.draw_contact(obj.jid, obj.conn.name)
|
|
else:
|
|
self.draw_pep(obj.jid, obj.conn.name, obj.pep_type)
|
|
|
|
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_gc_subject_received(self, obj):
|
|
contact = app.contacts.get_contact_with_highest_priority(
|
|
obj.conn.name, obj.jid)
|
|
if contact:
|
|
contact.status = obj.subject
|
|
self.draw_contact(obj.jid, obj.conn.name)
|
|
|
|
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.draw_account(obj.conn.name)
|
|
|
|
def _nec_decrypted_message_received(self, obj):
|
|
if not obj.msgtxt:
|
|
return True
|
|
if obj.mtype not in ('normal', 'chat'):
|
|
return
|
|
if obj.mtype == 'normal' and obj.popup:
|
|
# it's single message to be autopopuped
|
|
SingleMessageWindow(obj.conn.name, obj.jid,
|
|
action='receive', from_whom=obj.jid, subject=obj.subject,
|
|
message=obj.msgtxt, resource=obj.resource, session=obj.session,
|
|
form_node=obj.form_node)
|
|
return
|
|
|
|
if obj.popup and obj.mtype == 'chat' 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 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_bookmark_menuitem_activate(self, widget, account, jid, bookmark):
|
|
app.interface.join_gc_room(
|
|
account, jid, bookmark['nick'], bookmark['password'])
|
|
|
|
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_agent_logging(self, widget, jid, state, account):
|
|
"""
|
|
When an agent is requested to log in or off
|
|
"""
|
|
app.connections[account].send_agent_status(jid, state)
|
|
|
|
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.config.get_per('accounts', 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(list_):
|
|
for (contact, account) in list_:
|
|
full_jid = contact.get_full_jid()
|
|
app.connections[account].unsubscribe_agent(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 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 to contacts from these transports: %s') % jids
|
|
ConfirmationDialog(pritext, sectext,
|
|
on_response_ok = (remove, list_), transient_for=self.window)
|
|
|
|
def _nec_blocking(self, obj):
|
|
for jid in obj.changed:
|
|
self.draw_contact(jid, obj.conn.name)
|
|
|
|
def on_block(self, widget, list_, group=None):
|
|
"""
|
|
When clicked on the 'block' button in context menu. list_ is a list of
|
|
(contact, account)
|
|
"""
|
|
def on_continue(msg, pep_dict):
|
|
if msg is None:
|
|
# user pressed Cancel to change status message dialog
|
|
return
|
|
|
|
accounts = []
|
|
for _, account in list_:
|
|
con = app.connections[account]
|
|
if con.get_module('PrivacyLists').supported or (
|
|
group is None and con.get_module('Blocking').supported):
|
|
accounts.append(account)
|
|
|
|
if group is None:
|
|
for acct in accounts:
|
|
l_ = [i[0] for i in list_ if i[1] == acct]
|
|
con = app.connections[acct]
|
|
con.get_module('PrivacyLists').block_contacts(l_, msg)
|
|
for contact in l_:
|
|
self.draw_contact(contact.jid, acct)
|
|
else:
|
|
for acct in accounts:
|
|
l_ = [i[0] for i in list_ if i[1] == acct]
|
|
con = app.connections[acct]
|
|
con.get_module('PrivacyLists').block_group(group, l_, msg)
|
|
self.draw_group(group, acct)
|
|
for contact in l_:
|
|
self.draw_contact(contact.jid, acct)
|
|
|
|
def _block_it(is_checked=None):
|
|
if is_checked is not None: # dialog has been shown
|
|
if is_checked: # user does not want to be asked again
|
|
app.config.set('confirm_block', 'no')
|
|
else:
|
|
app.config.set('confirm_block', 'yes')
|
|
self.get_status_message('offline', on_continue, show_pep=False)
|
|
|
|
confirm_block = app.config.get('confirm_block')
|
|
if confirm_block == 'no':
|
|
_block_it()
|
|
return
|
|
pritext = _('You are about to block a contact. Are you sure you want'
|
|
' to continue?')
|
|
sectext = _('This contact will see you offline and you will not '
|
|
'receive messages it sends you.')
|
|
ConfirmationDialogCheck(pritext, sectext,
|
|
_('_Do not ask me again'), on_response_ok=_block_it)
|
|
|
|
def on_unblock(self, widget, list_, group=None):
|
|
"""
|
|
When clicked on the 'unblock' button in context menu.
|
|
"""
|
|
accounts = []
|
|
for _, account in list_:
|
|
con = app.connections[account]
|
|
if con.get_module('PrivacyLists').supported or (
|
|
group is None and con.get_module('Blocking').supported):
|
|
accounts.append(account)
|
|
|
|
if group is None:
|
|
for acct in accounts:
|
|
l_ = [i[0] for i in list_ if i[1] == acct]
|
|
con = app.connections[acct]
|
|
con.get_module('PrivacyLists').unblock_contacts(l_)
|
|
for contact in l_:
|
|
self.draw_contact(contact.jid, acct)
|
|
else:
|
|
for acct in accounts:
|
|
l_ = [i[0] for i in list_ if i[1] == acct]
|
|
con = app.connections[acct]
|
|
con.get_module('PrivacyLists').unblock_group(group, l_)
|
|
self.draw_group(group, acct)
|
|
for contact in l_:
|
|
self.draw_contact(contact.jid, acct)
|
|
for acct in accounts:
|
|
if 'privacy_list_block' in app.interface.instances[acct]:
|
|
del app.interface.instances[acct]['privacy_list_block']
|
|
|
|
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 app.connections[account].connected < 2:
|
|
return
|
|
if row_type in ('contact', 'agent'):
|
|
# it's jid
|
|
title = _('Rename Contact')
|
|
message = _('Enter a new nickname for contact %s') % jid
|
|
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')
|
|
message = _('Enter a new name for group %s') % \
|
|
GLib.markup_escape_text(jid)
|
|
|
|
def on_renamed(new_text, account, row_type, jid, old_text):
|
|
if 'rename' in app.interface.instances:
|
|
del app.interface.instances['rename']
|
|
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
|
|
app.connections[account].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)
|
|
|
|
def on_canceled():
|
|
if 'rename' in app.interface.instances:
|
|
del app.interface.instances['rename']
|
|
|
|
app.interface.instances['rename'] = InputDialog(title,
|
|
message, old_text, False, (on_renamed, account, row_type, jid,
|
|
old_text), on_canceled, transient_for=self.window)
|
|
|
|
def on_remove_group_item_activated(self, widget, group, account):
|
|
def on_ok(checked):
|
|
for contact in app.contacts.get_contacts_from_group(account,
|
|
group):
|
|
if not 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)
|
|
|
|
ConfirmationDialogCheck(_('Remove Group'),
|
|
_('Do you want to remove group %s from the roster?') % group,
|
|
_('Also remove all contacts in this group from your roster'),
|
|
on_response_ok=on_ok)
|
|
|
|
def on_assign_pgp_key(self, widget, contact, account):
|
|
attached_keys = app.config.get_per('accounts', account,
|
|
'attached_gpg_keys').split()
|
|
keys = {}
|
|
keyID = _('None')
|
|
for i in range(len(attached_keys) // 2):
|
|
keys[attached_keys[2*i]] = attached_keys[2*i+1]
|
|
if attached_keys[2*i] == contact.jid:
|
|
keyID = attached_keys[2*i+1]
|
|
public_keys = app.connections[account].ask_gpg_keys()
|
|
public_keys[_('None')] = _('None')
|
|
|
|
def on_key_selected(keyID):
|
|
if keyID is None:
|
|
return
|
|
if keyID[0] == _('None'):
|
|
if contact.jid in keys:
|
|
del keys[contact.jid]
|
|
keyID = ''
|
|
else:
|
|
keyID = keyID[0]
|
|
keys[contact.jid] = keyID
|
|
|
|
ctrl = app.interface.msg_win_mgr.get_control(contact.jid, account)
|
|
if ctrl:
|
|
ctrl.update_ui()
|
|
|
|
keys_str = ''
|
|
for jid in keys:
|
|
keys_str += jid + ' ' + keys[jid] + ' '
|
|
app.config.set_per('accounts', account, 'attached_gpg_keys',
|
|
keys_str)
|
|
for u in app.contacts.get_contacts(account, contact.jid):
|
|
u.keyID = helpers.prepare_and_validate_gpg_keyID(account,
|
|
contact.jid, keyID)
|
|
|
|
dialogs.ChooseGPGKeyDialog(_('Assign OpenPGP Key'),
|
|
_('Select a key to apply to the contact'), public_keys,
|
|
on_key_selected, selected=keyID, transient_for=self.window)
|
|
|
|
def on_edit_groups(self, widget, list_):
|
|
dialogs.EditGroupsDialog(list_)
|
|
|
|
def on_history(self, widget, contact, account):
|
|
"""
|
|
When history menuitem is activated: call log window
|
|
"""
|
|
if 'logs' in app.interface.instances:
|
|
app.interface.instances['logs'].window.present()
|
|
app.interface.instances['logs'].open_history(contact.jid, account)
|
|
else:
|
|
app.interface.instances['logs'] = HistoryWindow(contact.jid, account)
|
|
|
|
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.shutdown()
|
|
ctrl.got_disconnected()
|
|
self.remove_groupchat(jid, account)
|
|
|
|
def on_reconnect(self, widget, jid, account):
|
|
"""
|
|
When reconnect menuitem is activated: join the room
|
|
"""
|
|
if jid in app.interface.minimized_controls[account]:
|
|
ctrl = app.interface.minimized_controls[account][jid]
|
|
app.interface.join_gc_room(account, jid, ctrl.nick,
|
|
app.gc_passwords.get(jid, ''))
|
|
|
|
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_new_room(self, widget, list_, resource=None):
|
|
"""
|
|
Resource parameter MUST NOT be used if more than one contact in list
|
|
"""
|
|
account_list = []
|
|
jid_list = []
|
|
for (contact, account) in list_:
|
|
if contact.jid not in jid_list:
|
|
if resource: # we MUST have one contact only in list_
|
|
fjid = contact.jid + '/' + resource
|
|
jid_list.append(fjid)
|
|
else:
|
|
jid_list.append(contact.jid)
|
|
if account not in account_list:
|
|
account_list.append(account)
|
|
# transform None in 'jabber'
|
|
type_ = app.get_transport_name_from_jid(jid_list[0]) or 'jabber'
|
|
for account in account_list:
|
|
if app.connections[account].muc_jid[type_]:
|
|
# create the room on this muc server
|
|
if 'join_gc' in app.interface.instances[account]:
|
|
app.interface.instances[account]['join_gc'].destroy()
|
|
else:
|
|
app.interface.instances[account]['join_gc'] = \
|
|
JoinGroupchatWindow(
|
|
account, None, automatic={'invities': jid_list})
|
|
break
|
|
|
|
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
|
|
"""
|
|
for e in list_:
|
|
contact = e[0]
|
|
contact_jid = contact.jid
|
|
if resource: # we MUST have one contact only in list_
|
|
contact_jid += '/' + resource
|
|
con = app.connections[room_account]
|
|
con.get_module('MUC').invite(room_jid, contact_jid)
|
|
gc_control = app.interface.msg_win_mgr.get_gc_control(room_jid,
|
|
room_account)
|
|
if gc_control:
|
|
gc_control.print_conversation(
|
|
_('%(jid)s has been invited in this room') % {
|
|
'jid': contact_jid}, graphics=False)
|
|
|
|
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)
|
|
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_edit_account(self, widget, account):
|
|
window = app.get_app_window(AccountsWindow)
|
|
if window is None:
|
|
window = AccountsWindow()
|
|
else:
|
|
window.present()
|
|
window.select_account(account)
|
|
|
|
def on_change_status_message_activate(self, widget, account):
|
|
show = app.SHOW_LIST[app.connections[account].connected]
|
|
def on_response(message, pep_dict):
|
|
if message is None: # None is if user pressed Cancel
|
|
return
|
|
self.send_status(account, show, message)
|
|
self.send_pep(account, pep_dict)
|
|
dialogs.ChangeStatusMessageDialog(on_response, show)
|
|
|
|
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_connected(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
|
|
elif 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 on_roster_treeview_button_release_event(self, widget, event):
|
|
try:
|
|
path = self.tree.get_path_at_pos(int(event.x), int(event.y))[0]
|
|
except TypeError:
|
|
return False
|
|
|
|
if event.button == 1: # Left click
|
|
if app.single_click and not event.get_state() & Gdk.ModifierType.SHIFT_MASK and \
|
|
not event.get_state() & Gdk.ModifierType.CONTROL_MASK:
|
|
# Check if button has been pressed on the same row
|
|
if self.clicked_path == path:
|
|
self.on_row_activated(widget, path)
|
|
self.clicked_path = None
|
|
|
|
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
|
|
model = self.status_combobox.get_model()
|
|
accounts = list(app.connections.keys())
|
|
status = model[self.previous_status_combobox_active][2]
|
|
def on_response(message, pep_dict):
|
|
if message is not None: # None if user pressed Cancel
|
|
for account in accounts:
|
|
if not app.config.get_per('accounts', account,
|
|
'sync_with_global_status'):
|
|
continue
|
|
current_show = app.SHOW_LIST[
|
|
app.connections[account].connected]
|
|
self.send_status(account, current_show, message)
|
|
self.send_pep(account, pep_dict)
|
|
dialogs.ChangeStatusMessageDialog(on_response, status)
|
|
return True
|
|
elif 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)
|
|
|
|
elif 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':
|
|
show = app.connections[account].connected
|
|
if show > 1: # We are connected
|
|
self.on_change_status_message_activate(widget, account)
|
|
return True
|
|
show = helpers.get_global_show()
|
|
if show == 'offline':
|
|
return True
|
|
def on_response(message, pep_dict):
|
|
if message is None:
|
|
return True
|
|
for acct in app.connections:
|
|
if not app.config.get_per('accounts', acct,
|
|
'sync_with_global_status'):
|
|
continue
|
|
current_show = app.SHOW_LIST[app.connections[acct].\
|
|
connected]
|
|
self.send_status(acct, current_show, message)
|
|
self.send_pep(acct, pep_dict)
|
|
dialogs.ChangeStatusMessageDialog(on_response, show)
|
|
return True
|
|
|
|
elif 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.config.get('avatar_position_in_roster') == 'left':
|
|
x_min = AvatarSize.ROSTER
|
|
else:
|
|
x_min = 0
|
|
if app.single_click and not event.get_state() & Gdk.ModifierType.SHIFT_MASK and \
|
|
not event.get_state() & Gdk.ModifierType.CONTROL_MASK:
|
|
# Don't handle double click if we press icon of a metacontact
|
|
titer = model.get_iter(path)
|
|
if x > x_min and x < x_min + 27 and type_ == 'contact' and \
|
|
model.iter_has_child(titer):
|
|
if (self.tree.row_expanded(path)):
|
|
self.tree.collapse_row(path)
|
|
else:
|
|
self.tree.expand_row(path, False)
|
|
return
|
|
# We just save on which row we press button, and open chat
|
|
# window on button release to be able to do DND without opening
|
|
# chat window
|
|
self.clicked_path = path
|
|
return
|
|
else:
|
|
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 > x_min and 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, list_):
|
|
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 Roster') 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(list_):
|
|
on_ok(False, list_)
|
|
|
|
if len(list_) == 1:
|
|
contact = list_[0][0]
|
|
pritext = _('Contact "%s" will be removed from your roster') % \
|
|
contact.get_shown_name()
|
|
sectext = _('You are about to remove "%(name)s" (%(jid)s) from '
|
|
'your roster.\n') % {'name': contact.get_shown_name(),
|
|
'jid': contact.jid}
|
|
if contact.sub == 'to':
|
|
ConfirmationDialog(pritext, sectext + \
|
|
_('By removing this contact you also remove authorization '
|
|
'resulting in them always seeing you as offline.'),
|
|
on_response_ok=(on_ok2, list_))
|
|
elif _('Not in Roster') in contact.get_shown_groups():
|
|
# Contact is not in roster
|
|
ConfirmationDialog(pritext, sectext + \
|
|
_('Do you want to continue?'), on_response_ok=(on_ok2,
|
|
list_))
|
|
else:
|
|
ConfirmationDialogCheck(pritext, sectext + \
|
|
_('By removing this contact you also by default remove '
|
|
'authorization resulting in them always seeing you as'
|
|
' offline.'),
|
|
_('I want this contact to know my status after removal'),
|
|
on_response_ok=(on_ok, list_))
|
|
else:
|
|
# several contact to remove at the same time
|
|
pritext = _('Contacts will be removed from your roster')
|
|
jids = ''
|
|
for (contact, account) in list_:
|
|
jids += '\n ' + contact.get_shown_name() + ' (%s)' % \
|
|
contact.jid + ','
|
|
sectext = _('By removing these contacts:%s\nyou also remove '
|
|
'authorization resulting in them always seeing you as '
|
|
'offline.') % jids
|
|
ConfirmationDialog(pritext, sectext,
|
|
on_response_ok=(on_ok2, list_))
|
|
|
|
def on_send_custom_status(self, widget, contact_list, show, group=None):
|
|
"""
|
|
Send custom status
|
|
"""
|
|
# contact_list has only one element except if group != None
|
|
def on_response(message, pep_dict):
|
|
if message is None: # None if user pressed Cancel
|
|
return
|
|
account_list = []
|
|
for (contact, account) in contact_list:
|
|
if account not in account_list:
|
|
account_list.append(account)
|
|
# 1. update status_sent_to_[groups|users] list
|
|
if group:
|
|
for account in account_list:
|
|
if account not in app.interface.status_sent_to_groups:
|
|
app.interface.status_sent_to_groups[account] = {}
|
|
app.interface.status_sent_to_groups[account][group] = show
|
|
else:
|
|
for (contact, account) in contact_list:
|
|
if account not in app.interface.status_sent_to_users:
|
|
app.interface.status_sent_to_users[account] = {}
|
|
app.interface.status_sent_to_users[account][contact.jid] \
|
|
= show
|
|
|
|
# 2. update privacy lists if main status is invisible
|
|
for account in account_list:
|
|
if app.SHOW_LIST[app.connections[account].connected] == \
|
|
'invisible':
|
|
con = app.connections[account]
|
|
con.get_module('PrivacyLists').set_invisible_rule()
|
|
|
|
# 3. send directed presence
|
|
for (contact, account) in contact_list:
|
|
our_jid = app.get_jid_from_account(account)
|
|
jid = contact.jid
|
|
if jid == our_jid:
|
|
jid += '/' + contact.resource
|
|
self.send_status(account, show, message, to=jid)
|
|
|
|
def send_it(is_checked=None):
|
|
if is_checked is not None: # dialog has been shown
|
|
if is_checked: # user does not want to be asked again
|
|
app.config.set('confirm_custom_status', 'no')
|
|
else:
|
|
app.config.set('confirm_custom_status', 'yes')
|
|
self.get_status_message(show, on_response, show_pep=False,
|
|
always_ask=True)
|
|
|
|
confirm_custom_status = app.config.get('confirm_custom_status')
|
|
if confirm_custom_status == 'no':
|
|
send_it()
|
|
return
|
|
pritext = _('You are about to send a custom status. Are you sure you '
|
|
'want to continue?')
|
|
sectext = _('This contact will temporarily see you as %(status)s, '
|
|
'but only until you change your status. Then they will see '
|
|
'your global status.') % {'status': show}
|
|
ConfirmationDialogCheck(pritext, sectext,
|
|
_('_Do not ask me again'), on_response_ok=send_it)
|
|
|
|
def on_status_combobox_changed(self, widget):
|
|
"""
|
|
When we change our status via the combobox
|
|
"""
|
|
model = self.status_combobox.get_model()
|
|
active = self.status_combobox.get_active()
|
|
if active == -1: # no active item
|
|
return
|
|
if not self.combobox_callback_active:
|
|
self.previous_status_combobox_active = active
|
|
return
|
|
accounts = list(app.connections.keys())
|
|
if not accounts:
|
|
ErrorDialog(_('No account available'),
|
|
_('You must create an account before you can chat with other '
|
|
'contacts.'))
|
|
self.update_status_combobox()
|
|
return
|
|
status = model[active][2]
|
|
# status "desync'ed" or not
|
|
statuses_unified = helpers.statuses_unified()
|
|
if (active == 7 and statuses_unified) or (active == 9 and \
|
|
not statuses_unified):
|
|
# 'Change status message' selected:
|
|
# do not change show, just show change status dialog
|
|
status = model[self.previous_status_combobox_active][2]
|
|
def on_response(message, pep_dict):
|
|
if message is not None: # None if user pressed Cancel
|
|
for account in accounts:
|
|
if not app.config.get_per('accounts', account,
|
|
'sync_with_global_status'):
|
|
continue
|
|
current_show = app.SHOW_LIST[
|
|
app.connections[account].connected]
|
|
self.send_status(account, current_show, message)
|
|
self.send_pep(account, pep_dict)
|
|
self.combobox_callback_active = False
|
|
self.status_combobox.set_active(
|
|
self.previous_status_combobox_active)
|
|
self.combobox_callback_active = True
|
|
dialogs.ChangeStatusMessageDialog(on_response, status)
|
|
return
|
|
# we are about to change show, so save this new show so in case
|
|
# after user chooses "Change status message" menuitem
|
|
# we can return to this show
|
|
self.previous_status_combobox_active = active
|
|
connected_accounts = app.get_number_of_connected_accounts()
|
|
|
|
def on_continue(message, pep_dict):
|
|
if message is None:
|
|
# user pressed Cancel to change status message dialog
|
|
self.update_status_combobox()
|
|
return
|
|
global_sync_accounts = []
|
|
for acct in accounts:
|
|
if app.config.get_per('accounts', acct,
|
|
'sync_with_global_status'):
|
|
global_sync_accounts.append(acct)
|
|
global_sync_connected_accounts = \
|
|
app.get_number_of_connected_accounts(global_sync_accounts)
|
|
for account in accounts:
|
|
if not app.config.get_per('accounts', account,
|
|
'sync_with_global_status'):
|
|
continue
|
|
# we are connected (so we wanna change show and status)
|
|
# or no account is connected and we want to connect with new
|
|
# show and status
|
|
|
|
if not global_sync_connected_accounts > 0 or \
|
|
app.connections[account].connected > 0:
|
|
self.send_status(account, status, message)
|
|
self.send_pep(account, pep_dict)
|
|
self.update_status_combobox()
|
|
|
|
if status == 'invisible':
|
|
bug_user = False
|
|
for account in accounts:
|
|
if connected_accounts < 1 or app.account_is_connected(
|
|
account):
|
|
if not app.config.get_per('accounts', account,
|
|
'sync_with_global_status'):
|
|
continue
|
|
# We're going to change our status to invisible
|
|
if self.connected_rooms(account):
|
|
bug_user = True
|
|
break
|
|
if bug_user:
|
|
def on_ok():
|
|
self.get_status_message(status, on_continue, show_pep=False)
|
|
|
|
def on_cancel():
|
|
self.update_status_combobox()
|
|
|
|
ConfirmationDialog(
|
|
_('You are participating in one or more group chats'),
|
|
_('Changing your status to invisible will result in '
|
|
'disconnection from those group chats. Are you sure you '
|
|
'want to go invisible?'), on_response_ok=on_ok,
|
|
on_response_cancel=on_cancel)
|
|
return
|
|
|
|
self.get_status_message(status, on_continue)
|
|
|
|
def on_publish_tune_toggled(self, widget, account):
|
|
active = widget.get_active()
|
|
app.config.set_per('accounts', account, 'publish_tune', active)
|
|
if active:
|
|
app.interface.enable_music_listener()
|
|
else:
|
|
app.connections[account].get_module('UserTune').send(None)
|
|
# disable music listener only if no other account uses it
|
|
for acc in app.connections:
|
|
if app.config.get_per('accounts', acc, 'publish_tune'):
|
|
break
|
|
else:
|
|
app.interface.disable_music_listener()
|
|
|
|
helpers.update_optional_features(account)
|
|
|
|
def on_publish_location_toggled(self, widget, account):
|
|
active = widget.get_active()
|
|
app.config.set_per('accounts', account, 'publish_location', active)
|
|
if active:
|
|
location_listener.enable()
|
|
else:
|
|
app.connections[account].get_module('UserLocation').send(None)
|
|
|
|
helpers.update_optional_features(account)
|
|
|
|
def on_pep_services_menuitem_activate(self, widget, account):
|
|
if 'pep_services' in app.interface.instances[account]:
|
|
app.interface.instances[account]['pep_services'].window.present()
|
|
else:
|
|
app.interface.instances[account]['pep_services'] = \
|
|
ManagePEPServicesWindow(account)
|
|
|
|
def on_add_new_contact(self, widget, account):
|
|
AddNewContactWindow(account)
|
|
|
|
def on_join_gc_activate(self, widget, account):
|
|
"""
|
|
When the join gc menuitem is clicked, show the join gc window
|
|
"""
|
|
app.app.activate_action('%s-join-groupchat' % account,
|
|
GLib.Variant('s', account))
|
|
|
|
def on_show_transports_action(self, action, param):
|
|
app.config.set('show_transports_group', param.get_boolean())
|
|
action.set_state(param)
|
|
self.refilter_shown_roster_items()
|
|
|
|
def on_manage_bookmarks_menuitem_activate(self, widget):
|
|
ManageBookmarksWindow()
|
|
|
|
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 is not None:
|
|
jid = jid + '/' + resource
|
|
adhoc_commands.CommandWindow(account, jid)
|
|
|
|
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
|
|
gtkgui_helpers.set_unset_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.config.get(
|
|
'quit_on_roster_x_button') and ((app.interface.systray_enabled and\
|
|
app.config.get('trayicon') == 'always') or app.config.get(
|
|
'allow_hide_roster')):
|
|
if os.name == 'nt' or app.config.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:
|
|
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)
|
|
self.on_history(widget, contact, account)
|
|
|
|
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
|
|
"""
|
|
if not app.single_click:
|
|
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] = \
|
|
app.interface.jabber_state_images['16']['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] = app.interface.\
|
|
jabber_state_images['16']['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.config.get_per('accounts', 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
|
|
disco.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.config.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.config.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.config.set('roster_width', roster_width)
|
|
app.config.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(NS_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):
|
|
WarningDialog(_('Metacontacts storage not supported by '
|
|
'your server'),
|
|
_('Your server does not support storing metacontacts '
|
|
'information. So this information will not be saved on next '
|
|
'reconnection.'))
|
|
|
|
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.config.set('confirm_metacontacts', 'no')
|
|
else:
|
|
app.config.set('confirm_metacontacts', 'yes')
|
|
|
|
# We might have dropped on a metacontact.
|
|
# Remove it and readd 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. Readd them all
|
|
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)
|
|
app.connections[account_source].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.config.get('confirm_metacontacts')
|
|
if confirm_metacontacts == 'no' or dest_family == source_family:
|
|
merge_contacts()
|
|
return
|
|
pritext = _('You are about to create a metacontact. Are you sure you '
|
|
'want to continue?')
|
|
sectext = _('Metacontacts are a way to regroup several contacts in one '
|
|
'line. Generally it is used when the same person has several '
|
|
'XMPP- or transport -accounts.')
|
|
dlg = ConfirmationDialogCheck(pritext, sectext,
|
|
_('_Do not ask me again'), on_response_ok=merge_contacts)
|
|
if not confirm_metacontacts: # First time we see this window
|
|
dlg.checkbutton.set_active(True)
|
|
|
|
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 app.connections[account_dest].connected < 2:
|
|
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(NS_FILE):
|
|
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
|
|
prim_text = 'Send file?'
|
|
sec_text = i18n.ngettext('Do you want to send this file to %s:',
|
|
'Do you want to send these files to %s:', nb_uri) %\
|
|
c_dest.get_shown_name()
|
|
for uri in uri_splitted:
|
|
path = helpers.get_file_path_from_dnd_dropped_uri(uri)
|
|
sec_text += '\n' + os.path.basename(path)
|
|
dialog = NonModalConfirmationDialog(prim_text, sec_text,
|
|
on_response_ok=(_on_send_files, account_dest, jid_dest,
|
|
uri_splitted))
|
|
dialog.popup()
|
|
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.config.get_per('accounts', 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
|
|
# TODO: Invite to groupchat if type_dest = contact
|
|
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]
|
|
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.config.get_per('accounts', 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 helpers.special_groups and \
|
|
grp_source not in ('Not in Roster', 'Observers'):
|
|
# 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, 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 :
|
|
return
|
|
|
|
# Is the contact we drag a meta contact?
|
|
accounts = (self.regroup and 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 get_appropriate_state_images(self, jid, size='16', icon_name='online'):
|
|
"""
|
|
Check jid and return the appropriate state images dict for the demanded
|
|
size. icon_name is taken into account when jid is from transport:
|
|
transport iconset doesn't contain all icons, so we fall back to jabber
|
|
one
|
|
"""
|
|
transport = app.get_transport_name_from_jid(jid)
|
|
if transport and size in self.transports_state_images:
|
|
if transport not in self.transports_state_images[size]:
|
|
# we don't have iconset for this transport loaded yet. Let's do
|
|
# it
|
|
self.make_transport_state_images(transport)
|
|
if transport in self.transports_state_images[size] and \
|
|
icon_name in self.transports_state_images[size][transport]:
|
|
return self.transports_state_images[size][transport]
|
|
return app.interface.jabber_state_images[size]
|
|
|
|
def make_transport_state_images(self, transport):
|
|
"""
|
|
Initialize opened and closed 'transport' iconset dict
|
|
"""
|
|
if not app.config.get('use_transports_iconsets'):
|
|
return
|
|
|
|
folder = os.path.join(helpers.get_transport_path(transport), '32x32')
|
|
self.transports_state_images['32'][transport] = \
|
|
gtkgui_helpers.load_iconset(folder, transport=True)
|
|
folder = os.path.join(helpers.get_transport_path(transport), '16x16')
|
|
self.transports_state_images['16'][transport] = \
|
|
gtkgui_helpers.load_iconset(folder, transport=True)
|
|
|
|
pixo, pixc = gtkgui_helpers.load_icons_meta()
|
|
self.transports_state_images['opened'][transport] = \
|
|
gtkgui_helpers.load_iconset(folder, pixo, transport=True)
|
|
self.transports_state_images['closed'][transport] = \
|
|
gtkgui_helpers.load_iconset(folder, pixc, transport=True)
|
|
|
|
def update_jabber_state_images(self):
|
|
# Update the roster
|
|
self.setup_and_draw_roster()
|
|
# Update the status combobox
|
|
self.status_combobox.queue_draw()
|
|
# Update the systray
|
|
if app.interface.systray_enabled:
|
|
app.interface.systray.set_img()
|
|
|
|
for win in app.interface.msg_win_mgr.windows():
|
|
for ctrl in win.controls():
|
|
ctrl.update_ui()
|
|
win.redraw_tab(ctrl)
|
|
|
|
self.update_status_combobox()
|
|
|
|
def set_account_status_icon(self, account):
|
|
status = app.connections[account].connected
|
|
child_iterA = self._get_account_iter(account, self.model)
|
|
if not child_iterA:
|
|
return
|
|
if not self.regroup:
|
|
show = app.SHOW_LIST[status]
|
|
else: # accounts merged
|
|
show = helpers.get_global_show()
|
|
self.model[child_iterA][Column.IMG] = app.interface.jabber_state_images[
|
|
'16'][show]
|
|
|
|
################################################################################
|
|
### Style and theme related methods
|
|
################################################################################
|
|
|
|
def show_title(self):
|
|
change_title_allowed = app.config.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.config.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')
|
|
gtkgui_helpers.set_unset_urgency_hint(self.window, nb_unread)
|
|
return
|
|
|
|
start = ''
|
|
if nb_unread > 1:
|
|
start = '[' + str(nb_unread) + '] '
|
|
elif nb_unread == 1:
|
|
start = '* '
|
|
|
|
self.window.set_title(start + 'Gajim')
|
|
|
|
gtkgui_helpers.set_unset_urgency_hint(self.window, nb_unread)
|
|
|
|
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
|
|
"""
|
|
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.2)
|
|
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
|
|
theme = app.config.get('roster_theme')
|
|
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)
|
|
renderer.set_property('foreground', color)
|
|
if not color:
|
|
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 not model[titer][data] or model[titer][data] == empty_pixbuf:
|
|
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.config.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.config.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):
|
|
theme = app.config.get('roster_theme')
|
|
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):
|
|
theme = app.config.get('roster_theme')
|
|
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):
|
|
theme = app.config.get('roster_theme')
|
|
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
|
|
iconset = app.config.get('iconset')
|
|
path = os.path.join(helpers.get_iconset_path(iconset), '16x16')
|
|
|
|
if not app.config.get_per('accounts', account, 'is_zeroconf'):
|
|
xml = gtkgui_helpers.get_gtk_builder('account_context_menu.ui')
|
|
account_context_menu = xml.get_object('account_context_menu')
|
|
|
|
status_menuitem = xml.get_object('status_menuitem')
|
|
join_group_chat_menuitem = xml.get_object(
|
|
'join_group_chat_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')
|
|
edit_account_menuitem = xml.get_object('edit_account_menuitem')
|
|
sub_menu = Gtk.Menu()
|
|
status_menuitem.set_submenu(sub_menu)
|
|
|
|
for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'):
|
|
uf_show = helpers.get_uf_show(show, use_mnemonic=True)
|
|
item = Gtk.MenuItem.new_with_mnemonic(uf_show)
|
|
sub_menu.append(item)
|
|
con = app.connections[account]
|
|
if show == 'invisible' and con.connected > 1 and \
|
|
not con.get_module('PrivacyLists').supported:
|
|
item.set_sensitive(False)
|
|
else:
|
|
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 app.connections[account].connected < 2:
|
|
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(_('Publish Tune'))
|
|
pep_submenu.append(item)
|
|
if sys.platform != 'linux':
|
|
item.set_sensitive(False)
|
|
else:
|
|
activ = app.config.get_per('accounts', account,
|
|
'publish_tune')
|
|
item.set_active(activ)
|
|
item.connect('toggled', self.on_publish_tune_toggled,
|
|
account)
|
|
|
|
item = Gtk.CheckMenuItem(_('Publish Location'))
|
|
pep_submenu.append(item)
|
|
if not app.is_installed('GEOCLUE'):
|
|
item.set_sensitive(False)
|
|
else:
|
|
activ = app.config.get_per('accounts', account,
|
|
'publish_location')
|
|
item.set_active(activ)
|
|
item.connect('toggled', self.on_publish_location_toggled,
|
|
account)
|
|
|
|
pep_config = Gtk.MenuItem.new_with_label(
|
|
_('Configure Services…'))
|
|
item = Gtk.SeparatorMenuItem.new()
|
|
pep_submenu.append(item)
|
|
pep_config.set_sensitive(True)
|
|
pep_submenu.append(pep_config)
|
|
pep_config.connect('activate',
|
|
self.on_pep_services_menuitem_activate, account)
|
|
|
|
else:
|
|
pep_menuitem.set_sensitive(False)
|
|
|
|
edit_account_menuitem.connect('activate', self.on_edit_account,
|
|
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.config.get_per('accounts', account, 'hostname')
|
|
contact = app.contacts.create_contact(jid=hostname,
|
|
account=account) # Fake contact
|
|
execute_command_menuitem.connect('activate',
|
|
self.on_execute_command, contact, account)
|
|
|
|
gc_sub_menu = Gtk.Menu() # gc is always a submenu
|
|
join_group_chat_menuitem.set_submenu(gc_sub_menu)
|
|
self.add_bookmarks_list(gc_sub_menu, account)
|
|
|
|
# make some items insensitive if account is offline
|
|
if app.connections[account].connected < 2:
|
|
for widget in (add_contact_menuitem, service_discovery_menuitem,
|
|
join_group_chat_menuitem, execute_command_menuitem,
|
|
pep_menuitem):
|
|
widget.set_sensitive(False)
|
|
else:
|
|
xml = gtkgui_helpers.get_gtk_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', 'invisible'):
|
|
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 app.connections[account].connected < 2:
|
|
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.connect('activate',
|
|
self.on_edit_account, 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()
|
|
iconset = app.config.get('iconset')
|
|
path = os.path.join(helpers.get_iconset_path(iconset), '16x16')
|
|
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, titer):
|
|
"""
|
|
Make group's popup menu
|
|
"""
|
|
model = self.modelfilter
|
|
path = model.get_path(titer)
|
|
group = model[titer][Column.JID]
|
|
account = model[titer][Column.ACCOUNT]
|
|
|
|
list_ = [] # list of (contact, account) tuples
|
|
list_online = [] # list of (contact, account) tuples
|
|
|
|
show_bookmarked = True
|
|
group = model[titer][Column.JID]
|
|
for jid in app.contacts.get_jid_list(account):
|
|
contact = app.contacts.get_contact_with_highest_priority(account,
|
|
jid)
|
|
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(NS_CONFERENCE):
|
|
show_bookmarked = False
|
|
list_.append((contact, account))
|
|
menu = Gtk.Menu()
|
|
|
|
# Make special context menu if group is Groupchats
|
|
if group == _('Groupchats'):
|
|
maximize_menuitem = Gtk.MenuItem.new_with_mnemonic(_(
|
|
'_Maximize All'))
|
|
maximize_menuitem.connect('activate',
|
|
self.on_all_groupchat_maximized, list_)
|
|
menu.append(maximize_menuitem)
|
|
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
|
|
if group != _('Transports'):
|
|
invite_menuitem = Gtk.MenuItem.new_with_mnemonic(
|
|
_('In_vite to'))
|
|
|
|
gui_menu_builder.build_invite_submenu(invite_menuitem,
|
|
list_online, show_bookmarked=show_bookmarked)
|
|
menu.append(invite_menuitem)
|
|
|
|
# Send Custom Status
|
|
send_custom_status_menuitem = Gtk.MenuItem.new_with_mnemonic(
|
|
_('Send Cus_tom Status'))
|
|
if helpers.group_is_blocked(account, group):
|
|
send_custom_status_menuitem.set_sensitive(False)
|
|
status_menuitems = Gtk.Menu()
|
|
send_custom_status_menuitem.set_submenu(status_menuitems)
|
|
iconset = app.config.get('iconset')
|
|
path = os.path.join(helpers.get_iconset_path(iconset), '16x16')
|
|
for s in ('online', 'chat', 'away', 'xa', 'dnd', 'offline'):
|
|
status_menuitem = Gtk.MenuItem.new_with_label(
|
|
helpers.get_uf_show(s))
|
|
status_menuitem.connect('activate', self.on_send_custom_status,
|
|
list_, s, group)
|
|
status_menuitems.append(status_menuitem)
|
|
menu.append(send_custom_status_menuitem)
|
|
|
|
# there is no singlemessage and custom status for zeroconf
|
|
if app.config.get_per('accounts', account, 'is_zeroconf'):
|
|
send_custom_status_menuitem.set_sensitive(False)
|
|
send_group_message_item.set_sensitive(False)
|
|
|
|
if app.connections[account].connected < 2:
|
|
send_group_message_item.set_sensitive(False)
|
|
invite_menuitem.set_sensitive(False)
|
|
send_custom_status_menuitem.set_sensitive(False)
|
|
|
|
if not group in helpers.special_groups:
|
|
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)
|
|
|
|
# Block group
|
|
is_blocked = False
|
|
if self.regroup:
|
|
for g_account in app.connections:
|
|
if helpers.group_is_blocked(g_account, group):
|
|
is_blocked = True
|
|
else:
|
|
if helpers.group_is_blocked(account, group):
|
|
is_blocked = True
|
|
|
|
if is_blocked and app.connections[account].get_module('PrivacyLists').supported:
|
|
unblock_menuitem = Gtk.MenuItem.new_with_mnemonic(_('_Unblock'))
|
|
unblock_menuitem.connect('activate', self.on_unblock, list_,
|
|
group)
|
|
menu.append(unblock_menuitem)
|
|
else:
|
|
block_menuitem = Gtk.MenuItem.new_with_mnemonic(_('_Block'))
|
|
block_menuitem.connect('activate', self.on_block, list_, group)
|
|
menu.append(block_menuitem)
|
|
if not app.connections[account].get_module('PrivacyLists').supported:
|
|
block_menuitem.set_sensitive(False)
|
|
|
|
# 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 app.connections[account].connected < 2:
|
|
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
|
|
privacy_rules_supported = True
|
|
for titer in iters:
|
|
jid = model[titer][Column.JID]
|
|
account = model[titer][Column.ACCOUNT]
|
|
if app.connections[account].connected < 2:
|
|
one_account_offline = True
|
|
if not app.connections[account].get_module('PrivacyLists').supported:
|
|
privacy_rules_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(NS_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 privacy_rules_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 privacy_rules_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]
|
|
path = model.get_path(titer)
|
|
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)
|
|
|
|
if not app.gc_connected[account].get(jid, False):
|
|
connect_menuitem = Gtk.MenuItem.new_with_mnemonic(_(
|
|
'_Reconnect'))
|
|
connect_menuitem.connect('activate', self.on_reconnect, jid,
|
|
account)
|
|
menu.append(connect_menuitem)
|
|
disconnect_menuitem = Gtk.MenuItem.new_with_mnemonic(_(
|
|
'_Disconnect'))
|
|
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.connect('activate', self.on_history, contact, account)
|
|
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 get_and_connect_advanced_menuitem_menu(self, account):
|
|
"""
|
|
Add FOR ACCOUNT options
|
|
"""
|
|
xml = gtkgui_helpers.get_gtk_builder('advanced_menuitem_menu.ui')
|
|
advanced_menuitem_menu = xml.get_object('advanced_menuitem_menu')
|
|
|
|
xml_console_menuitem = xml.get_object('xml_console_menuitem')
|
|
archiving_preferences_menuitem = xml.get_object(
|
|
'archiving_preferences_menuitem')
|
|
privacy_lists_menuitem = xml.get_object('privacy_lists_menuitem')
|
|
administrator_menuitem = xml.get_object('administrator_menuitem')
|
|
send_server_message_menuitem = xml.get_object(
|
|
'send_server_message_menuitem')
|
|
set_motd_menuitem = xml.get_object('set_motd_menuitem')
|
|
update_motd_menuitem = xml.get_object('update_motd_menuitem')
|
|
delete_motd_menuitem = xml.get_object('delete_motd_menuitem')
|
|
|
|
xml_console_menuitem.connect('activate',
|
|
self.on_xml_console_menuitem_activate, account)
|
|
|
|
if app.connections[account]:
|
|
if app.connections[account].get_module('PrivacyLists').supported:
|
|
privacy_lists_menuitem.connect('activate',
|
|
self.on_privacy_lists_menuitem_activate, account)
|
|
else:
|
|
privacy_lists_menuitem.set_sensitive(False)
|
|
if app.connections[account].get_module('MAM').available:
|
|
archiving_preferences_menuitem.connect(
|
|
'activate',
|
|
self.on_archiving_preferences_menuitem_activate, account)
|
|
else:
|
|
archiving_preferences_menuitem.set_sensitive(False)
|
|
|
|
if app.connections[account].is_zeroconf:
|
|
administrator_menuitem.set_sensitive(False)
|
|
send_server_message_menuitem.set_sensitive(False)
|
|
set_motd_menuitem.set_sensitive(False)
|
|
update_motd_menuitem.set_sensitive(False)
|
|
delete_motd_menuitem.set_sensitive(False)
|
|
else:
|
|
send_server_message_menuitem.connect('activate',
|
|
self.on_send_server_message_menuitem_activate, account)
|
|
|
|
set_motd_menuitem.connect('activate',
|
|
self.on_set_motd_menuitem_activate, account)
|
|
|
|
update_motd_menuitem.connect('activate',
|
|
self.on_update_motd_menuitem_activate, account)
|
|
|
|
delete_motd_menuitem.connect('activate',
|
|
self.on_delete_motd_menuitem_activate, account)
|
|
|
|
advanced_menuitem_menu.show_all()
|
|
|
|
return advanced_menuitem_menu
|
|
|
|
def add_history_manager_menuitem(self, menu):
|
|
"""
|
|
Add a separator and History Manager menuitem BELOW for account menuitems
|
|
"""
|
|
item = Gtk.SeparatorMenuItem.new() # separator
|
|
menu.append(item)
|
|
|
|
# History manager
|
|
item = Gtk.MenuItem.new_with_mnemonic(_('History Manager'))
|
|
menu.append(item)
|
|
item.connect('activate', self.on_history_manager_menuitem_activate)
|
|
|
|
def add_bookmarks_list(self, gc_sub_menu, account):
|
|
"""
|
|
Show join new group chat item and bookmarks list for an account
|
|
"""
|
|
item = Gtk.MenuItem.new_with_mnemonic(_('_Join New Group Chat'))
|
|
item.connect('activate', self.on_join_gc_activate, account)
|
|
|
|
gc_sub_menu.append(item)
|
|
|
|
# User has at least one bookmark.
|
|
con = app.connections[account]
|
|
if con.get_module('Bookmarks').bookmarks:
|
|
item = Gtk.SeparatorMenuItem.new()
|
|
gc_sub_menu.append(item)
|
|
|
|
bookmarks = con.get_module('Bookmarks').get_sorted_bookmarks(
|
|
short_name=True)
|
|
for jid, bookmark in bookmarks.items():
|
|
name = bookmark['name']
|
|
|
|
# Do not use underline.
|
|
item = Gtk.MenuItem.new_with_label(name)
|
|
item.set_use_underline(False)
|
|
item.connect(
|
|
'activate', self.on_bookmark_menuitem_activate,
|
|
account, jid, bookmark)
|
|
gc_sub_menu.append(item)
|
|
|
|
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' and len(iters) == 1:
|
|
self.make_group_menu(event, iters[0])
|
|
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 on_ctrl_j(self, accel_group, acceleratable, keyval, modifier):
|
|
"""
|
|
Bring up the conference join dialog, when CTRL+J accelerator is being
|
|
activated
|
|
"""
|
|
# find a connected account:
|
|
for account in app.connections:
|
|
if account == 'Local':
|
|
continue
|
|
if app.account_is_connected(account):
|
|
self.on_join_gc_activate(None, account)
|
|
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.keys():
|
|
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:
|
|
row = widget.get_path_at_pos(x_pos, y_pos)[0]
|
|
except TypeError:
|
|
return False
|
|
if not row:
|
|
return False
|
|
|
|
iter_ = None
|
|
try:
|
|
model = widget.get_model()
|
|
iter_ = model.get_iter(row)
|
|
except Exception:
|
|
return False
|
|
|
|
typ = model[iter_][Column.TYPE]
|
|
account = model[iter_][Column.ACCOUNT]
|
|
jid = model[iter_][Column.JID]
|
|
connected_contacts = []
|
|
|
|
if typ in ('contact', 'self_contact'):
|
|
contacts = app.contacts.get_contacts(account, jid)
|
|
|
|
for c in contacts:
|
|
if c.show not in ('offline', 'error'):
|
|
connected_contacts.append(c)
|
|
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
|
|
|
|
if self.current_tooltip != row:
|
|
# If the row changes we hide the current tooltip
|
|
self.current_tooltip = row
|
|
return False
|
|
|
|
tooltip = widget.get_tooltip_window()
|
|
|
|
if tooltip.row == row:
|
|
# We already populated the window with the row data
|
|
return True
|
|
tooltip.row = row
|
|
tooltip.populate(connected_contacts, account, typ)
|
|
return True
|
|
|
|
def add_actions(self):
|
|
action = Gio.SimpleAction.new_stateful(
|
|
"show-roster", None,
|
|
GLib.Variant.new_boolean(
|
|
not self.xml.get_object('roster_vbox2').get_no_show_all()))
|
|
action.connect("change-state",
|
|
self.on_show_roster_action)
|
|
self.window.add_action(action)
|
|
|
|
action = Gio.SimpleAction.new_stateful(
|
|
"show-offline", None,
|
|
GLib.Variant.new_boolean(app.config.get('showoffline')))
|
|
action.connect("change-state",
|
|
self.on_show_offline_contacts_action)
|
|
self.window.add_action(action)
|
|
|
|
action = Gio.SimpleAction.new_stateful(
|
|
"show-active", None,
|
|
GLib.Variant.new_boolean(
|
|
app.config.get('show_only_chat_and_online')))
|
|
action.connect("change-state",
|
|
self.on_show_active_contacts_action)
|
|
self.window.add_action(action)
|
|
|
|
action = Gio.SimpleAction.new_stateful(
|
|
"show-transports", None,
|
|
GLib.Variant.new_boolean(app.config.get('show_transports_group')))
|
|
action.connect("change-state", self.on_show_transports_action)
|
|
self.window.add_action(action)
|
|
|
|
################################################################################
|
|
###
|
|
################################################################################
|
|
|
|
def __init__(self, application: Gtk.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, rememver if we already saved config once
|
|
self.save_done = False
|
|
|
|
# [icon, name, type, jid, account, editable, mood_pixbuf,
|
|
# activity_pixbuf, tune_pixbuf, location_pixbuf, avatar_img,
|
|
# padlock_pixbuf, visible]
|
|
self.columns = [Gtk.Image, str, str, str, str,
|
|
GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf,
|
|
Gtk.Image, str, bool]
|
|
|
|
self.xml = gtkgui_helpers.get_gtk_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.config.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.config.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
|
|
gtkgui_helpers.resize_window(self.window,
|
|
app.config.get('roster_width'),
|
|
app.config.get('roster_height'))
|
|
if app.config.get('save-roster-position'):
|
|
gtkgui_helpers.move_window(self.window,
|
|
app.config.get('roster_x-position'),
|
|
app.config.get('roster_y-position'))
|
|
|
|
self.popups_notification_height = 0
|
|
self.popup_notification_windows = []
|
|
|
|
# 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 = []
|
|
|
|
# StatusComboBox
|
|
self.status_combobox = self.xml.get_object('status_combobox')
|
|
pixbuf_renderer, text_renderer = self.status_combobox.get_cells()
|
|
self.status_combobox.set_cell_data_func(
|
|
pixbuf_renderer, self._status_cell_data_func)
|
|
self.status_combobox.set_cell_data_func(
|
|
text_renderer, self._status_cell_data_func)
|
|
self.status_combobox.set_row_separator_func(self._iter_is_separator)
|
|
|
|
self.status_combobox.set_active(9)
|
|
# holds index to previously selected item so if
|
|
# "change status message..." is selected we can fallback to previously
|
|
# selected item and not stay with that item selected
|
|
self.previous_status_combobox_active = 9
|
|
|
|
# Enable/Disable checkboxes at start
|
|
if app.config.get('showoffline'):
|
|
self.window.lookup_action('show-active').set_enabled(False)
|
|
|
|
if app.config.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 ={}
|
|
self._pep_type_to_model_column = {'mood': Column.MOOD_PIXBUF,
|
|
'activity': Column.ACTIVITY_PIXBUF, 'tune': Column.TUNE_PIXBUF,
|
|
'geoloc': Column.LOCATION_PIXBUF}
|
|
|
|
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.config.get('avatar_position_in_roster') == 'left':
|
|
add_avatar_renderer()
|
|
|
|
self.renderers_list += (
|
|
('icon', cell_renderer_image.CellRendererImage(0, 0), False,
|
|
'image', Column.IMG, self._iconCellDataFunc, None),
|
|
|
|
('name', renderer_text, True,
|
|
'markup', Column.NAME, self._nameCellDataFunc, None),
|
|
|
|
('mood', Gtk.CellRendererPixbuf(), False,
|
|
'pixbuf', Column.MOOD_PIXBUF,
|
|
self._fill_pep_pixbuf_renderer, Column.MOOD_PIXBUF),
|
|
|
|
('activity', Gtk.CellRendererPixbuf(), False,
|
|
'pixbuf', Column.ACTIVITY_PIXBUF,
|
|
self._fill_pep_pixbuf_renderer, Column.ACTIVITY_PIXBUF),
|
|
|
|
('tune', Gtk.CellRendererPixbuf(), False,
|
|
'pixbuf', Column.TUNE_PIXBUF,
|
|
self._fill_pep_pixbuf_renderer, Column.TUNE_PIXBUF),
|
|
|
|
('geoloc', Gtk.CellRendererPixbuf(), False,
|
|
'pixbuf', Column.LOCATION_PIXBUF,
|
|
self._fill_pep_pixbuf_renderer, Column.LOCATION_PIXBUF))
|
|
|
|
if app.config.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
|
|
self.TARGET_TYPE_URI_LIST = 80
|
|
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()
|
|
self.tree.enable_model_drag_dest([], Gdk.DragAction.DEFAULT)
|
|
dst_targets = Gtk.TargetList.new([])
|
|
dst_targets.add_text_targets(0)
|
|
dst_targets.add_uri_targets(self.TARGET_TYPE_URI_LIST)
|
|
self.tree.drag_dest_set_target_list(dst_targets)
|
|
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.config.get('collapsed_rows').split('\t')
|
|
self.tree.set_has_tooltip(True)
|
|
self.tree.set_tooltip_window(tooltips.RosterTooltip(self.window))
|
|
self.current_tooltip = None
|
|
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.config.get('show_roster_on_startup') == 'always':
|
|
self.window.show_all()
|
|
elif app.config.get('show_roster_on_startup') == 'never':
|
|
if app.config.get('trayicon') != 'always':
|
|
# Without trayicon, user should see the roster!
|
|
self.window.show_all()
|
|
app.config.set('last_roster_visible', True)
|
|
else:
|
|
if app.config.get('last_roster_visible') or \
|
|
app.config.get('trayicon') != 'always':
|
|
self.window.show_all()
|
|
|
|
self.scale_factor = self.window.get_scale_factor()
|
|
|
|
if not app.config.get_per('accounts') or \
|
|
app.config.get_per('accounts') == ['Local'] and not \
|
|
app.config.get_per('accounts', 'Local', 'active'):
|
|
# if we have no account configured or only Local account but not enabled
|
|
def _open_wizard():
|
|
app.interface.instances['account_creation_wizard'] = \
|
|
AccountCreationWizard()
|
|
# Open wizard only after roster is created, so we can make it
|
|
# transient for the roster window
|
|
GLib.idle_add(_open_wizard)
|
|
|
|
# Setting CTRL+J to be the shortcut for bringing up the dialog to join a
|
|
# conference.
|
|
accel_group = Gtk.accel_groups_from_object(self.window)[0]
|
|
accel_group.connect(Gdk.KEY_j, Gdk.ModifierType.CONTROL_MASK,
|
|
Gtk.AccelFlags.MASK, self.on_ctrl_j)
|
|
|
|
# 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)
|
|
# presence has to be fully handled so that contact is added to occupant
|
|
# list before roster can be correctly updated
|
|
app.ged.register_event_handler('gc-presence-received', ged.GUI2,
|
|
self._nec_gc_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.GUI1,
|
|
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('pep-received', ged.GUI1,
|
|
self._nec_pep_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('gc-subject-received', ged.GUI1,
|
|
self._nec_gc_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)
|