356 lines
12 KiB
Python
356 lines
12 KiB
Python
|
import logging
|
||
|
|
||
|
from gi.repository import Gio
|
||
|
from gi.repository import Gdk
|
||
|
from gi.repository import Gtk
|
||
|
from gi.repository import GLib
|
||
|
|
||
|
from nbxmpp.errors import StanzaError
|
||
|
from nbxmpp.namespaces import Namespace
|
||
|
from nbxmpp.modules.vcard4 import VCard
|
||
|
from nbxmpp.modules.user_avatar import Avatar
|
||
|
|
||
|
from gajim.common import app
|
||
|
from gajim.common.const import AvatarSize
|
||
|
from gajim.common.i18n import _
|
||
|
from gajim.common.i18n import Q_
|
||
|
|
||
|
from gajim.gui.avatar import clip_circle
|
||
|
from gajim.gui.avatar_selector import AvatarSelector
|
||
|
from gajim.gui.dialogs import ErrorDialog
|
||
|
from gajim.gui.filechoosers import AvatarChooserDialog
|
||
|
from gajim.gui.util import get_builder
|
||
|
from gajim.gui.vcard_grid import VCardGrid
|
||
|
from gajim.gui.util import scroll_to_end
|
||
|
|
||
|
log = logging.getLogger('gajim.gui.profile')
|
||
|
|
||
|
MENU_DICT = {
|
||
|
'fn': Q_('?profile:Full Name'),
|
||
|
'bday': _('Birthday'),
|
||
|
'gender': Q_('?profile:Gender'),
|
||
|
'adr': Q_('?profile:Address'),
|
||
|
'email': _('Email'),
|
||
|
'impp': Q_('?profile:IM Address'),
|
||
|
'tel': _('Phone No.'),
|
||
|
'org': Q_('?profile:Organisation'),
|
||
|
'title': Q_('?profile:Title'),
|
||
|
'role': Q_('?profile:Role'),
|
||
|
'url': _('URL'),
|
||
|
'key': Q_('?profile:Public Encryption Key'),
|
||
|
'note': Q_('?profile:Note'),
|
||
|
}
|
||
|
|
||
|
|
||
|
class ProfileWindow(Gtk.ApplicationWindow):
|
||
|
def __init__(self, account, *args):
|
||
|
Gtk.ApplicationWindow.__init__(self)
|
||
|
self.set_application(app.app)
|
||
|
self.set_position(Gtk.WindowPosition.CENTER)
|
||
|
self.set_show_menubar(False)
|
||
|
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
||
|
self.set_resizable(True)
|
||
|
self.set_default_size(700, 600)
|
||
|
self.set_name('ProfileWindow')
|
||
|
self.set_title(_('Profile'))
|
||
|
|
||
|
self.account = account
|
||
|
self._jid = app.get_jid_from_account(account)
|
||
|
|
||
|
self._ui = get_builder('profile.ui')
|
||
|
|
||
|
menu = Gio.Menu()
|
||
|
for action, label in MENU_DICT.items():
|
||
|
menu.append(label, 'win.add-' + action.lower())
|
||
|
|
||
|
self._ui.add_entry_button.set_menu_model(menu)
|
||
|
self._add_actions()
|
||
|
|
||
|
self._avatar_selector = None
|
||
|
self._current_avatar = None
|
||
|
self._current_vcard = None
|
||
|
self._avatar_nick_public = None
|
||
|
|
||
|
# False - no change to avatar
|
||
|
# None - we want to delete the avatar
|
||
|
# Avatar - upload new avatar
|
||
|
self._new_avatar = False
|
||
|
|
||
|
self._ui.nickname_entry.set_text(app.nicks[account])
|
||
|
|
||
|
self._vcard_grid = VCardGrid(self.account)
|
||
|
self._ui.profile_box.add(self._vcard_grid)
|
||
|
|
||
|
self.add(self._ui.profile_stack)
|
||
|
self.show_all()
|
||
|
|
||
|
self._load_avatar()
|
||
|
|
||
|
client = app.get_client(account)
|
||
|
client.get_module('VCard4').request_vcard(
|
||
|
callback=self._on_vcard_received)
|
||
|
|
||
|
client.get_module('PubSub').get_access_model(
|
||
|
Namespace.VCARD4_PUBSUB,
|
||
|
callback=self._on_access_model_received,
|
||
|
user_data=Namespace.VCARD4_PUBSUB)
|
||
|
|
||
|
client.get_module('PubSub').get_access_model(
|
||
|
Namespace.AVATAR_METADATA,
|
||
|
callback=self._on_access_model_received,
|
||
|
user_data=Namespace.AVATAR_METADATA)
|
||
|
|
||
|
client.get_module('PubSub').get_access_model(
|
||
|
Namespace.AVATAR_DATA,
|
||
|
callback=self._on_access_model_received,
|
||
|
user_data=Namespace.AVATAR_DATA)
|
||
|
|
||
|
client.get_module('PubSub').get_access_model(
|
||
|
Namespace.NICK,
|
||
|
callback=self._on_access_model_received,
|
||
|
user_data=Namespace.NICK)
|
||
|
|
||
|
self._ui.connect_signals(self)
|
||
|
self.connect('key-press-event', self._on_key_press_event)
|
||
|
|
||
|
def _on_access_model_received(self, task):
|
||
|
namespace = task.get_user_data()
|
||
|
|
||
|
try:
|
||
|
result = task.finish()
|
||
|
except StanzaError as error:
|
||
|
log.warning('Unable to get access model for %s: %s',
|
||
|
namespace, error)
|
||
|
return
|
||
|
|
||
|
access_model = result == 'open'
|
||
|
|
||
|
if namespace == Namespace.VCARD4_PUBSUB:
|
||
|
self._set_vcard_access_switch(access_model)
|
||
|
else:
|
||
|
if self._avatar_nick_public is None:
|
||
|
self._avatar_nick_public = access_model
|
||
|
else:
|
||
|
self._avatar_nick_public = (self._avatar_nick_public or
|
||
|
access_model)
|
||
|
self._set_avatar_nick_access_switch(self._avatar_nick_public)
|
||
|
|
||
|
def _on_vcard_received(self, task):
|
||
|
try:
|
||
|
self._current_vcard = task.finish()
|
||
|
except StanzaError as error:
|
||
|
log.info('Error loading VCard: %s', error)
|
||
|
self._current_vcard = VCard()
|
||
|
|
||
|
if self._current_vcard is None:
|
||
|
self._current_vcard = VCard()
|
||
|
|
||
|
self._load_avatar()
|
||
|
self._vcard_grid.set_vcard(self._current_vcard.copy())
|
||
|
self._ui.profile_stack.set_visible_child_name('profile')
|
||
|
self._ui.spinner.stop()
|
||
|
|
||
|
def _load_avatar(self):
|
||
|
scale = self.get_scale_factor()
|
||
|
self._current_avatar = app.contacts.get_avatar(
|
||
|
self.account,
|
||
|
self._jid,
|
||
|
AvatarSize.VCARD,
|
||
|
scale)
|
||
|
|
||
|
self._ui.avatar_image.set_from_surface(self._current_avatar)
|
||
|
self._ui.avatar_image.show()
|
||
|
|
||
|
def _on_key_press_event(self, _widget, event):
|
||
|
if event.keyval == Gdk.KEY_Escape:
|
||
|
self.destroy()
|
||
|
|
||
|
def _add_actions(self):
|
||
|
for action in MENU_DICT:
|
||
|
action_name = 'add-' + action.lower()
|
||
|
act = Gio.SimpleAction.new(action_name, None)
|
||
|
act.connect('activate', self._on_action)
|
||
|
self.add_action(act)
|
||
|
|
||
|
def _on_action(self, action, _param):
|
||
|
name = action.get_name()
|
||
|
key = name.split('-')[1]
|
||
|
self._vcard_grid.add_new_property(key)
|
||
|
GLib.idle_add(scroll_to_end, self._ui.scrolled)
|
||
|
|
||
|
def _on_edit_clicked(self, *args):
|
||
|
self._vcard_grid.set_editable(True)
|
||
|
self._ui.edit_button.hide()
|
||
|
self._ui.add_entry_button.set_no_show_all(False)
|
||
|
self._ui.add_entry_button.show_all()
|
||
|
self._ui.cancel_button.show()
|
||
|
self._ui.save_button.show()
|
||
|
self._ui.remove_avatar_button.show()
|
||
|
self._ui.edit_avatar_button.show()
|
||
|
self._ui.nickname_entry.set_sensitive(True)
|
||
|
self._ui.privacy_button.show()
|
||
|
|
||
|
def _on_cancel_clicked(self, _widget):
|
||
|
self._vcard_grid.set_editable(False)
|
||
|
self._ui.edit_button.show()
|
||
|
self._ui.add_entry_button.hide()
|
||
|
self._ui.cancel_button.hide()
|
||
|
self._ui.save_button.hide()
|
||
|
self._ui.remove_avatar_button.hide()
|
||
|
self._ui.edit_avatar_button.hide()
|
||
|
self._ui.privacy_button.hide()
|
||
|
self._ui.nickname_entry.set_sensitive(False)
|
||
|
self._ui.avatar_image.set_from_surface(self._current_avatar)
|
||
|
self._ui.nickname_entry.set_text(app.nicks[self.account])
|
||
|
self._vcard_grid.set_vcard(self._current_vcard.copy())
|
||
|
self._new_avatar = False
|
||
|
|
||
|
def _on_save_clicked(self, _widget):
|
||
|
self._ui.spinner.start()
|
||
|
self._ui.profile_stack.set_visible_child_name('spinner')
|
||
|
self._ui.add_entry_button.hide()
|
||
|
self._ui.cancel_button.hide()
|
||
|
self._ui.save_button.hide()
|
||
|
self._ui.edit_button.show()
|
||
|
self._ui.remove_avatar_button.hide()
|
||
|
self._ui.edit_avatar_button.hide()
|
||
|
self._ui.privacy_button.hide()
|
||
|
self._ui.nickname_entry.set_sensitive(False)
|
||
|
|
||
|
self._vcard_grid.validate()
|
||
|
self._vcard_grid.sort()
|
||
|
|
||
|
vcard = self._vcard_grid.get_vcard()
|
||
|
self._current_vcard = vcard.copy()
|
||
|
|
||
|
con = app.connections[self.account]
|
||
|
con.get_module('VCard4').set_vcard(
|
||
|
self._current_vcard,
|
||
|
public=self._ui.vcard_access.get_active(),
|
||
|
callback=self._on_save_finished)
|
||
|
|
||
|
public = self._ui.avatar_nick_access.get_active()
|
||
|
|
||
|
if self._new_avatar is False:
|
||
|
if self._avatar_nick_public != public:
|
||
|
con.get_module('UserAvatar').set_access_model(public)
|
||
|
|
||
|
else:
|
||
|
# Only update avatar if it changed
|
||
|
con.get_module('UserAvatar').set_avatar(
|
||
|
self._new_avatar,
|
||
|
public=public,
|
||
|
callback=self._on_set_avatar)
|
||
|
|
||
|
nick = GLib.markup_escape_text(self._ui.nickname_entry.get_text())
|
||
|
con.get_module('UserNickname').set_nickname(nick, public=public)
|
||
|
|
||
|
if not nick:
|
||
|
nick = app.settings.get_account_setting(
|
||
|
self.account, 'name')
|
||
|
app.nicks[self.account] = nick
|
||
|
|
||
|
def _on_set_avatar(self, task):
|
||
|
try:
|
||
|
task.finish()
|
||
|
except StanzaError as error:
|
||
|
if self._new_avatar is None:
|
||
|
# Trying to remove the avatar but the node does not exist
|
||
|
if error.condition == 'item-not-found':
|
||
|
return
|
||
|
|
||
|
title = _('Error while uploading avatar')
|
||
|
text = error.get_text()
|
||
|
|
||
|
if (error.condition == 'not-acceptable' and
|
||
|
error.app_condition == 'payload-too-big'):
|
||
|
text = _('Avatar file size too big')
|
||
|
|
||
|
ErrorDialog(title, text)
|
||
|
|
||
|
self._ui.avatar_image.set_from_surface(self._current_avatar)
|
||
|
self._new_avatar = False
|
||
|
return
|
||
|
|
||
|
def _on_remove_avatar(self, _button):
|
||
|
contact = app.contacts.create_contact(self._jid, self.account)
|
||
|
scale = self.get_scale_factor()
|
||
|
surface = app.interface.avatar_storage.get_surface(
|
||
|
contact, AvatarSize.VCARD, scale, default=True)
|
||
|
|
||
|
self._ui.avatar_image.set_from_surface(surface)
|
||
|
self._ui.remove_avatar_button.hide()
|
||
|
self._new_avatar = None
|
||
|
|
||
|
def _on_edit_avatar(self, button):
|
||
|
def _on_file_selected(path):
|
||
|
if self._avatar_selector is None:
|
||
|
self._avatar_selector = AvatarSelector()
|
||
|
self._ui.avatar_selector_box.add(self._avatar_selector)
|
||
|
|
||
|
self._avatar_selector.prepare_crop_area(path)
|
||
|
self._ui.avatar_update_button.set_sensitive(
|
||
|
self._avatar_selector.get_prepared())
|
||
|
self._ui.profile_stack.set_visible_child_name('avatar_selector')
|
||
|
|
||
|
AvatarChooserDialog(_on_file_selected,
|
||
|
transient_for=button.get_toplevel())
|
||
|
|
||
|
def _on_cancel_update_avatar(self, _button):
|
||
|
self._ui.profile_stack.set_visible_child_name('profile')
|
||
|
|
||
|
def _on_update_avatar(self, _button):
|
||
|
success, data, width, height = self._avatar_selector.get_avatar_bytes()
|
||
|
if not success:
|
||
|
self._ui.profile_stack.set_visible_child_name('profile')
|
||
|
ErrorDialog(_('Error while processing image'),
|
||
|
_('Failed to generate avatar.'))
|
||
|
return
|
||
|
|
||
|
sha = app.interface.avatar_storage.save_avatar(data)
|
||
|
if sha is None:
|
||
|
self._ui.profile_stack.set_visible_child_name('profile')
|
||
|
ErrorDialog(_('Error while processing image'),
|
||
|
_('Failed to generate avatar.'))
|
||
|
return
|
||
|
|
||
|
self._new_avatar = Avatar()
|
||
|
self._new_avatar.add_image_source(data, 'image/png', height, width)
|
||
|
|
||
|
scale = self.get_scale_factor()
|
||
|
surface = app.interface.avatar_storage.surface_from_filename(
|
||
|
sha, AvatarSize.VCARD, scale)
|
||
|
|
||
|
self._ui.avatar_image.set_from_surface(clip_circle(surface))
|
||
|
self._ui.remove_avatar_button.show()
|
||
|
self._ui.profile_stack.set_visible_child_name('profile')
|
||
|
|
||
|
def _set_vcard_access_switch(self, state):
|
||
|
self._ui.vcard_access.set_active(state)
|
||
|
self._ui.vcard_access_label.set_text(
|
||
|
_('Everyone') if state else _('Contacts'))
|
||
|
|
||
|
def _set_avatar_nick_access_switch(self, state):
|
||
|
self._ui.avatar_nick_access.set_active(state)
|
||
|
self._ui.avatar_nick_access_label.set_text(
|
||
|
_('Everyone') if state else _('Contacts'))
|
||
|
|
||
|
def _access_switch_toggled(self, *args):
|
||
|
state = self._ui.vcard_access.get_active()
|
||
|
self._set_vcard_access_switch(state)
|
||
|
|
||
|
state = self._ui.avatar_nick_access.get_active()
|
||
|
self._set_avatar_nick_access_switch(state)
|
||
|
|
||
|
def _on_save_finished(self, task):
|
||
|
try:
|
||
|
task.finish()
|
||
|
except StanzaError as err:
|
||
|
log.error('Could not publish VCard: %s', err)
|
||
|
# TODO Handle error
|
||
|
return
|
||
|
|
||
|
self._vcard_grid.set_editable(False)
|
||
|
self._ui.profile_stack.set_visible_child_name('profile')
|
||
|
self._ui.spinner.stop()
|