# HG changeset patch # User Goffi # Date 1424711302 -3600 # Node ID f29beedb33b0a5f5ed688700d972235c76ff0a40 # Parent 2ecc07a8f91b2089439a434cb646e0cb8cb6a7f8# Parent 0f92b6a150ff119a1f91c3505ed9fb25778b793e merged souliane changes diff -r 2ecc07a8f91b -r f29beedb33b0 frontends/src/constants.py --- a/frontends/src/constants.py Mon Feb 23 18:04:25 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,75 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# generic module for SàT frontends -# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program 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 Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -from sat.core import constants -from sat.core.i18n import _, D_ -from collections import OrderedDict # only available from python 2.7 - - -def getPresence(): - """We cannot do it directly in the Const class, if it is not encapsulated - in a method we get a JS runtime SyntaxError: "missing ) in parenthetical". - # TODO: merge this definition with those in primitivus.constants - """ - - -class Const(constants.Const): - - PRESENCE = OrderedDict([("", _("Online")), - ("chat", _("Free for chat")), - ("away", _("Away from keyboard")), - ("dnd", _("Do not disturb")), - ("xa", _("Extended away"))]) - - # from plugin_misc_text_syntaxes - SYNTAX_XHTML = "XHTML" - SYNTAX_CURRENT = "@CURRENT@" - SYNTAX_TEXT = "text" - - # XMLUI - SAT_FORM_PREFIX = "SAT_FORM_" - SAT_PARAM_SEPARATOR = "_XMLUI_PARAM_" # used to have unique elements names - XMLUI_STATUS_VALIDATED = "validated" - XMLUI_STATUS_CANCELLED = constants.Const.XMLUI_DATA_CANCELLED - - # MUC - ALL_OCCUPANTS = 1 - MUC_USER_STATES = { - "active": u'✔', - "inactive": u'☄', - "gone": u'✈', - "composing": u'✎', - "paused": u"⦷" - } - - # Roster - GROUP_NOT_IN_ROSTER = D_('Not in roster') - - # Chats - CHAT_ONE2ONE = 'one2one' - CHAT_GROUP = 'group' - - # Widgets management - # FIXME: should be in quick_frontend.constant, but Libervia doesn't inherit from it - WIDGET_NEW = 'NEW' - WIDGET_KEEP = 'KEEP' - WIDGET_RAISE = 'RAISE' - WIDGET_RECREATE = 'RECREATE' - diff -r 2ecc07a8f91b -r f29beedb33b0 frontends/src/primitivus/primitivus --- a/frontends/src/primitivus/primitivus Mon Feb 23 18:04:25 2015 +0100 +++ b/frontends/src/primitivus/primitivus Mon Feb 23 18:08:22 2015 +0100 @@ -464,7 +464,6 @@ def addContactList(self, profile): contact_list = ContactList(self, on_click=self.contactSelected, on_change=lambda w: self.redraw(), profile=profile) self.contact_lists_pile.contents.append((contact_list, ('weight', 1))) - self.contact_lists[profile] = contact_list return contact_list def plugging_profiles(self): diff -r 2ecc07a8f91b -r f29beedb33b0 frontends/src/primitivus/profile_manager.py --- a/frontends/src/primitivus/profile_manager.py Mon Feb 23 18:04:25 2015 +0100 +++ b/frontends/src/primitivus/profile_manager.py Mon Feb 23 18:08:22 2015 +0100 @@ -153,7 +153,7 @@ self.host.redraw() def alert(self, title, message): - popup = sat_widgets.alert(title, message, ok_cb=self.host.removePopUp) + popup = sat_widgets.Alert(title, message, ok_cb=self.host.removePopUp) self.host.showPopUp(popup) def onProfileChange(self, list_wid): diff -r 2ecc07a8f91b -r f29beedb33b0 frontends/src/primitivus/status.py --- a/frontends/src/primitivus/status.py Mon Feb 23 18:04:25 2015 +0100 +++ b/frontends/src/primitivus/status.py Mon Feb 23 18:08:22 2015 +0100 @@ -20,7 +20,7 @@ from sat.core.i18n import _ import urwid from urwid_satext import sat_widgets -from sat_frontends.constants import Const as commonConst +from sat_frontends.quick_frontend.constants import Const as commonConst from sat_frontends.primitivus.constants import Const diff -r 2ecc07a8f91b -r f29beedb33b0 frontends/src/quick_frontend/constants.py --- a/frontends/src/quick_frontend/constants.py Mon Feb 23 18:04:25 2015 +0100 +++ b/frontends/src/quick_frontend/constants.py Mon Feb 23 18:08:22 2015 +0100 @@ -17,18 +17,58 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sat_frontends import constants +from sat.core import constants +from sat.core.i18n import _, D_ +from collections import OrderedDict # only available from python 2.7 class Const(constants.Const): - #Contact list + PRESENCE = OrderedDict([("", _("Online")), + ("chat", _("Free for chat")), + ("away", _("Away from keyboard")), + ("dnd", _("Do not disturb")), + ("xa", _("Extended away"))]) + + # from plugin_misc_text_syntaxes + SYNTAX_XHTML = "XHTML" + SYNTAX_CURRENT = "@CURRENT@" + SYNTAX_TEXT = "text" + + # XMLUI + SAT_FORM_PREFIX = "SAT_FORM_" + SAT_PARAM_SEPARATOR = "_XMLUI_PARAM_" # used to have unique elements names + XMLUI_STATUS_VALIDATED = "validated" + XMLUI_STATUS_CANCELLED = constants.Const.XMLUI_DATA_CANCELLED + + # MUC + ALL_OCCUPANTS = 1 + MUC_USER_STATES = { + "active": u'✔', + "inactive": u'☄', + "gone": u'✈', + "composing": u'✎', + "paused": u"⦷" + } + + # Roster CONTACT_GROUPS = 'groups' CONTACT_RESOURCES = 'resources' CONTACT_MAIN_RESOURCE = 'main_resource' CONTACT_SPECIAL = 'special' - CONTACT_SPECIAL_GROUP = 'group' # group chat special entity - CONTACT_SPECIAL_ALLOWED = (CONTACT_SPECIAL_GROUP,) # set of allowed values for special flag - CONTACT_DATA_FORBIDDEN = {CONTACT_GROUPS, CONTACT_RESOURCES, CONTACT_MAIN_RESOURCE} # set of forbidden names for contact data + CONTACT_SPECIAL_GROUP = 'group' # group chat special entity + CONTACT_SPECIAL_ALLOWED = (CONTACT_SPECIAL_GROUP,) # set of allowed values for special flag + CONTACT_DATA_FORBIDDEN = {CONTACT_GROUPS, CONTACT_RESOURCES, CONTACT_MAIN_RESOURCE} # set of forbidden names for contact data + + # Chats + CHAT_ONE2ONE = 'one2one' + CHAT_GROUP = 'group' - LISTENERS = {'avatar'} + # Widgets management + # FIXME: should be in quick_frontend.constant, but Libervia doesn't inherit from it + WIDGET_NEW = 'NEW' + WIDGET_KEEP = 'KEEP' + WIDGET_RAISE = 'RAISE' + WIDGET_RECREATE = 'RECREATE' + + LISTENERS = {'avatar', 'presence'} diff -r 2ecc07a8f91b -r f29beedb33b0 frontends/src/quick_frontend/quick_app.py --- a/frontends/src/quick_frontend/quick_app.py Mon Feb 23 18:04:25 2015 +0100 +++ b/frontends/src/quick_frontend/quick_app.py Mon Feb 23 18:08:22 2015 +0100 @@ -17,16 +17,25 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sat.core.i18n import _ -import sys from sat.core.log import getLogger log = getLogger(__name__) + +from sat.core.i18n import _ from sat.core import exceptions + from sat_frontends.tools import jid from sat_frontends.quick_frontend.quick_widgets import QuickWidgetsManager from sat_frontends.quick_frontend import quick_chat +from sat_frontends.quick_frontend.constants import Const as C -from sat_frontends.quick_frontend.constants import Const as C +import sys +from collections import OrderedDict + +try: + # FIXME: to be removed when an acceptable solution is here + unicode('') # XXX: unicode doesn't exist in pyjamas +except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options + unicode = str class ProfileManager(object): @@ -75,11 +84,14 @@ def _plug_profile_gotCachedValues(self, cached_values): # TODO: watched plugin + + # add the contact list and its listener contact_list = self.host.addContactList(self.profile) + self.host.contact_lists[self.profile] = contact_list for entity, data in cached_values.iteritems(): for key, value in data.iteritems(): - self.host.contact_lists[self.profile].setCache(jid.JID(entity), key, value) + contact_list.setCache(jid.JID(entity), key, value) if not self.bridge.isConnected(self.profile): self.host.setStatusOnline(False, profile=self.profile) @@ -169,11 +181,18 @@ def unplug(self, profile): if profile not in self._profiles: raise ValueError('The profile [{}] is not plugged'.format(profile)) + + # remove the contact list and its listener + host = self._profiles[profile].host + host.contact_lists[profile].onDelete() + del host.contact_lists[profile] + del self._profiles[profile] def chooseOneProfile(self): return self._profiles.keys()[0] + class QuickApp(object): """This class contain the main methods needed for the frontend""" @@ -196,7 +215,7 @@ self.selected_widget = None # widget currently selected (must be filled by frontend) # listeners - self._listeners = {} # key: listerner type ("avatar", "selected", etc), value: list of callbacks + self._listeners = {} # key: listener type ("avatar", "selected", etc), value: list of callbacks ## bridge ## try: @@ -287,31 +306,38 @@ handler(*args, **kwargs) self.bridge.register(functionName, signalReceived, iface) - def addListerner(self, type_, callback): - """Add a listerner for an event + def addListener(self, type_, callback, profiles_filter=None): + """Add a listener for an event /!\ don't forget to remove listener when not used anymore (e.g. if you delete a widget) @param type_: type of event, can be: - avatar: called when avatar data is updated - args: (entity, avatar file, profile) + args: (entity, avatar file) + - presence: called when a presence is received + args: (entity, show, priority, statuses) @param callback: method to call on event + @param profiles_filter (set[unicode]): if set and not empty, the + listener will be callable only by one of the given profiles. """ assert type_ in C.LISTENERS - self._listeners.setdefault(type_, []).append(callback) + self._listeners.setdefault(type_, OrderedDict())[callback] = profiles_filter def removeListener(self, type_, callback): """Remove a callback from listeners - @param type_: same as for [addListerner] + @param type_: same as for [addListener] @param callback: callback to remove """ assert type_ in C.LISTENERS - self._listeners[type_].remove(callback) + self._listeners[type_].pop(callback) - def callListeners(self, type_, *args): - """Call all methods which listen of type_ event + def callListeners(self, type_, profile, *args): + """Call the methods which listen type_ event. If a profiles filter has + been register with a listener and profile argument is not None, the + listener will be called only if profile is in the profiles filter list. - @param type_: same as for [addListerner] + @param type_: same as for [addListener] + @param profile (unicode): %(doc_profile)s @param *args: arguments sent to callback """ assert type_ in C.LISTENERS @@ -320,8 +346,9 @@ except KeyError: pass else: - for listener in listeners: - listener(*args) + for listener, profiles_filter in listeners.iteritems(): + if profile is None or not profiles_filter or profile in profiles_filter: + listener(*args) def check_profile(self, profile): """Tell if the profile is currently followed by the application""" @@ -374,7 +401,7 @@ """Tell the application to not follow anymore the profile""" if not profile in self.profiles: raise ValueError("The profile [{}] is not plugged".format(profile)) - self.profiles.remove(profile) + self.profiles.unplug(profile) def clear_profile(self): self.profiles.clear() @@ -468,7 +495,7 @@ # if entity.bare in self.profiles[profile].data.get('watched',[]) and not entity.bare in self.profiles[profile]['onlineContact']: # self.showAlert(_("Watched jid [%s] is connected !") % entity.bare) - self.contact_lists[profile].updatePresence(entity, show, priority, statuses) + self.callListeners('presence', profile, entity, show, priority, statuses) def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile): """Called when a MUC room is joined""" @@ -614,10 +641,8 @@ def _subscribe_cb(self, answer, data): entity, profile = data - if answer: - self.bridge.subscription("subscribed", entity.bare, profile_key=profile) - else: - self.bridge.subscription("unsubscribed", entity.bare, profile_key=profile) + type_ = "subscribed" if answer else "unsubscribed" + self.bridge.subscription(type_, unicode(entity.bare), profile_key=profile) def subscribeHandler(self, type, raw_jid, profile): """Called when a subsciption management signal is received""" @@ -632,7 +657,7 @@ # this is a subscriptionn request, we have to ask for user confirmation self.showDialog(_("The contact %s wants to subscribe to your presence.\nDo you accept ?") % entity.bare, _('Subscription confirmation'), 'yes/no', answer_cb=self._subscribe_cb, answer_data=(entity, profile)) - def showDialog(self, message, title, type="info", answer_cb=None): + def showDialog(self, message, title, type="info", answer_cb=None, answer_data=None): raise NotImplementedError def showAlert(self, message): @@ -650,8 +675,8 @@ elif (namespace, name) == ('General', C.SHOW_EMPTY_GROUPS): self.contact_lists[profile].showEmptyGroups(C.bool(value)) - def contactDeletedHandler(self, jid, profile): - target = jid.JID(jid) + def contactDeletedHandler(self, jid_s, profile): + target = jid.JID(jid_s) self.contact_lists[profile].remove(target) def entityDataUpdatedHandler(self, entity_s, key, value, profile): @@ -663,7 +688,7 @@ if entity in self.contact_lists[profile]: def gotFilename(filename): self.contact_lists[profile].setCache(entity, 'avatar', filename) - self.callListeners('avatar', entity, filename, profile) + self.callListeners('avatar', profile, entity, filename) self.bridge.getAvatarFile(value, callback=gotFilename) def askConfirmationHandler(self, confirm_id, confirm_type, data, profile): @@ -687,8 +712,12 @@ def onExit(self): """Must be called when the frontend is terminating""" + to_unplug = [] for profile in self.profiles: if self.bridge.isConnected(profile): if C.bool(self.bridge.getParamA("autodisconnect", "Connection", profile_key=profile)): #The user wants autodisconnection self.bridge.disconnect(profile) + to_unplug.append(profile) + for profile in to_unplug: + self.unplug_profile(profile) diff -r 2ecc07a8f91b -r f29beedb33b0 frontends/src/quick_frontend/quick_contact_list.py --- a/frontends/src/quick_frontend/quick_contact_list.py Mon Feb 23 18:04:25 2015 +0100 +++ b/frontends/src/quick_frontend/quick_contact_list.py Mon Feb 23 18:08:22 2015 +0100 @@ -72,6 +72,10 @@ self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=profile, callback=self._showEmptyGroups) self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=profile, callback=self._showOfflineContacts) + # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) + self.presenceListener = self.updatePresence + self.host.addListener('presence', self.presenceListener, [profile]) + def __contains__(self, entity): """Check if entity is in contact list @@ -93,16 +97,26 @@ return self._roster @property + def roster_entities_connected(self): + """Return all the bare JIDs of the roster entities that are connected. + + @return: set(jid.JID) + """ + return set([entity for entity in self._roster if self.getCache(entity, C.PRESENCE_SHOW) is not None]) + + @property def roster_entities_by_group(self): - """Return a dictionary binding the roster groups to their entities bare JIDs. + """Return a dictionary binding the roster groups to their entities bare + JIDs. This also includes the empty group (None key). @return: dict{unicode: set(jid.JID)} """ return {group: self._groups[group]['jids'] for group in self._groups} @property - def roster_groups_by_entity(self, contact_jid_s): - """Return a dictionary binding the entities bare JIDs to their roster groups. + def roster_groups_by_entity(self): + """Return a dictionary binding the entities bare JIDs to their roster + groups. The empty group is filtered out. @return: dict{jid.JID: set(unicode)} """ @@ -212,6 +226,17 @@ assert special_type in C.CONTACT_SPECIAL_ALLOWED + (None,) self.setCache(entity, C.CONTACT_SPECIAL, special_type) + def getSpecials(self, special_type=None): + """Return all the bare JIDs of the special roster entities of the type + specified by special_type. If special_type is None, return all specials. + + @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to return all specials. + @return: set(jid.JID) + """ + if special_type is None: + return self._specials + return set([entity for entity in self._specials if self.getCache(entity, C.CONTACT_SPECIAL) == special_type]) + def clearContacts(self): """Clear all the contact list""" self.unselectAll() @@ -425,3 +450,7 @@ return self.show_resources = show self.update() + + def onDelete(self): + QuickWidget.onDelete(self) + self.host.removeListener('presence', self.presenceListener) diff -r 2ecc07a8f91b -r f29beedb33b0 frontends/src/quick_frontend/quick_profile_manager.py --- a/frontends/src/quick_frontend/quick_profile_manager.py Mon Feb 23 18:04:25 2015 +0100 +++ b/frontends/src/quick_frontend/quick_profile_manager.py Mon Feb 23 18:08:22 2015 +0100 @@ -113,7 +113,7 @@ self._autoconnect = False # manual mode msg = _("Trying to plug an unknown profile key ({})".format(profile_key)) log.warning(msg) - self.alert(_("Profile plugging in error"), msg, ok_cb=self.host.removePopUp) + self.alert(_("Profile plugging in error"), msg) break self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=profile) diff -r 2ecc07a8f91b -r f29beedb33b0 frontends/src/quick_frontend/quick_widgets.py --- a/frontends/src/quick_frontend/quick_widgets.py Mon Feb 23 18:04:25 2015 +0100 +++ b/frontends/src/quick_frontend/quick_widgets.py Mon Feb 23 18:08:22 2015 +0100 @@ -105,15 +105,13 @@ if there is neither 'profile' nor 'profiles', None will be used for 'profiles' if 'on_new_widget' is present it can have the following values: C.WIDGET_NEW [default]: self.host.newWidget will be called on widget creation - [callable]: this method will be called instead of self.host.newWidget, - and can return another widget to modify the result of the present method + [callable]: this method will be called instead of self.host.newWidget None: do nothing if 'on_existing_widget' is present it can have the following values: C.WIDGET_KEEP [default]: return the existing widget C.WIDGET_RAISE: raise WidgetAlreadyExistsError C.WIDGET_RECREATE: create a new widget *WITH A NEW HASH* - [callable]: this method will be called with existing widget as argument, - and can return another widget to modify the result of the present method + [callable]: this method will be called with existing widget as argument if 'force_hash' is present, the hash given in value will be used instead of the one returned by class_.getWidgetHash other keys will be used to instanciate class_ if the case happen (e.g. if type_ is present and class_ is a QuickChat subclass, it will be used to create a new QuickChat instance). @@ -171,9 +169,7 @@ if on_new_widget == C.WIDGET_NEW: self.host.newWidget(widget) elif callable(on_new_widget): - result = on_new_widget(widget) - if isinstance(result, QuickWidget): - widget = result + on_new_widget(widget) else: assert on_new_widget is None else: @@ -207,9 +203,7 @@ log.debug(u"Widget already exists, a new one has been recreated with hash {}".format(new_kwargs['force_hash'])) break elif callable(on_existing_widget): - result = on_existing_widget(widget) - if isinstance(result, QuickWidget): - widget = result + on_existing_widget(widget) else: raise exceptions.InternalError("Unexpected on_existing_widget value ({})".format(on_existing_widget)) diff -r 2ecc07a8f91b -r f29beedb33b0 frontends/src/tools/xmlui.py --- a/frontends/src/tools/xmlui.py Mon Feb 23 18:04:25 2015 +0100 +++ b/frontends/src/tools/xmlui.py Mon Feb 23 18:08:22 2015 +0100 @@ -20,7 +20,7 @@ from sat.core.i18n import _ from sat.core.log import getLogger log = getLogger(__name__) -from sat_frontends.constants import Const as C +from sat_frontends.quick_frontend.constants import Const as C from sat.core.exceptions import DataError diff -r 2ecc07a8f91b -r f29beedb33b0 src/core/sat_main.py --- a/src/core/sat_main.py Mon Feb 23 18:04:25 2015 +0100 +++ b/src/core/sat_main.py Mon Feb 23 18:08:22 2015 +0100 @@ -640,7 +640,7 @@ assert profile d1 = self.profiles[profile].roster.removeItem(to_jid) d2 = self.profiles[profile].presence.unsubscribe(to_jid) - d_list = defer.DefferedList([d1, d2]) + d_list = defer.DeferredList([d1, d2]) def check_result(list_result): for success, value in list_result: if not success: diff -r 2ecc07a8f91b -r f29beedb33b0 src/plugins/plugin_xep_0085.py --- a/src/plugins/plugin_xep_0085.py Mon Feb 23 18:04:25 2015 +0100 +++ b/src/plugins/plugin_xep_0085.py Mon Feb 23 18:08:22 2015 +0100 @@ -126,16 +126,16 @@ self.map[profile][to_jid]._onEvent('gone') del self.map[profile] - def updateEntityData(self, entity_jid, value, profile): + def updateCache(self, entity_jid, value, profile): """Update the entity data of the given profile for one or all contacts. + Reset the chat state(s) display if the notification has been disabled. - Reset the chat state(s) display if the notification has been disabled. @param entity_jid: contact's JID, or C.ENTITY_ALL to update all contacts. @param value: True, False or DELETE_VALUE to delete the entity data @param profile: current profile """ if value == DELETE_VALUE: - self.host.memory.delEntityData(entity_jid, ENTITY_KEY, profile) + self.host.memory.delEntityDatum(entity_jid, ENTITY_KEY, profile) else: self.host.memory.updateEntityData(entity_jid, ENTITY_KEY, value, profile_key=profile) if not value or value == DELETE_VALUE: @@ -151,7 +151,7 @@ @param type_: parameter type """ if (category, name) == (PARAM_KEY, PARAM_NAME): - self.updateEntityData(C.ENTITY_ALL, True if bool("true") else DELETE_VALUE, profile=profile) + self.updateCache(C.ENTITY_ALL, True if bool("true") else DELETE_VALUE, profile=profile) return False return True @@ -173,11 +173,11 @@ try: domish.generateElementsNamed(message.elements(), name="active").next() # contact enabled Chat State Notifications - self.updateEntityData(from_jid, True, profile=profile) + self.updateCache(from_jid, True, profile=profile) except StopIteration: if message.getAttribute('type') == 'chat': # contact didn't enable Chat State Notifications - self.updateEntityData(from_jid, False, profile=profile) + self.updateCache(from_jid, False, profile=profile) return True except StopIteration: pass @@ -260,7 +260,7 @@ except (exceptions.UnknownEntityError, KeyError): if forceEntityData: # enable it for the first time - self.updateEntityData(to_jid, True, profile=profile) + self.updateCache(to_jid, True, profile=profile) return True # wait for the first message before sending states return False