changeset 1972:02d21a589be2

quick_frontend, primitivus: notifications refactoring replaced old "alerts" system by a more generic one which use listeners and can activate callbacks on notification click.
author Goffi <goffi@goffi.org>
date Mon, 27 Jun 2016 22:36:22 +0200
parents 9421e721d5e2
children a9908e751c42
files frontends/src/primitivus/constants.py frontends/src/primitivus/contact_list.py frontends/src/primitivus/game_tarot.py frontends/src/primitivus/primitivus frontends/src/quick_frontend/constants.py frontends/src/quick_frontend/quick_app.py frontends/src/quick_frontend/quick_chat.py frontends/src/quick_frontend/quick_contact_list.py
diffstat 8 files changed, 131 insertions(+), 139 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/src/primitivus/constants.py	Mon Jun 27 21:45:13 2016 +0200
+++ b/frontends/src/primitivus/constants.py	Mon Jun 27 22:36:22 2016 +0200
@@ -31,8 +31,8 @@
                ('selected_focus', 'default,bold', 'dark red'),
                ('default', 'default', 'default'),
                ('default_focus', 'default,bold', 'default'),
-               ('alert', 'default,underline', 'default'),
-               ('alert_focus', 'default,bold,underline', 'default'),
+               ('cl_notifs', 'default,underline', 'yellow'),
+               ('cl_notifs_focus', 'default,bold,underline', 'yellow'),
                # Messages
                ('date', 'light gray', 'default'),
                ('my_nick', 'dark red,bold', 'default'),
@@ -100,6 +100,3 @@
     MODE_COMMAND = 'COMMAND'
 
     GROUP_DATA_FOLDED = 'folded'
-
-    # contacts and contact list
-    ALERT_HEADER='(%i) '
--- a/frontends/src/primitivus/contact_list.py	Mon Jun 27 21:45:13 2016 +0200
+++ b/frontends/src/primitivus/contact_list.py	Mon Jun 27 22:36:22 2016 +0200
@@ -144,18 +144,16 @@
         """Method called when a contact is clicked
 
         @param use_bare_jid: True if use_bare_jid is set in self._buildEntityWidget.
-            If True, all jids in self._alerts with the same bare jid has contact_wid.data will be removed
         @param contact_wid: widget of the contact, must have the entity set in data attribute
         @param selected: boolean returned by the widget, telling if it is selected
         """
         entity = contact_wid.data
-        self.contact_list.removeAlerts(entity, use_bare_jid)
         self.host.modeHint(C.MODE_INSERTION)
         self._emit('click', entity)
 
     # Methods to build the widget
 
-    def _buildEntityWidget(self, entity, keys=None, use_bare_jid=False, with_alert=True, with_show_attr=True, markup_prepend=None, markup_append = None):
+    def _buildEntityWidget(self, entity, keys=None, use_bare_jid=False, with_notifs=True, with_show_attr=True, markup_prepend=None, markup_append = None):
         """Build one contact markup data
 
         @param entity (jid.JID): entity to build
@@ -164,8 +162,8 @@
             If key starts with "cache_", it will be checked in cache,
             else, getattr will be done on entity with the key (e.g. getattr(entity, 'node')).
             If nothing full or keys is None, full entity is used.
-        @param use_bare_jid (bool): if True, use bare jid for alerts and selected comparisons
-        @param with_alert (bool): if True, show alert if entity is in self._alerts
+        @param use_bare_jid (bool): if True, use bare jid for selected comparisons
+        @param with_notifs (bool): if True, show notification count
         @param with_show_attr (bool): if True, show color corresponding to presence status
         @param markup_prepend (list): markup to prepend to the generated one before building the widget
         @param markup_append (list): markup to append to the generated one before building the widget
@@ -199,12 +197,12 @@
         else:
             entity_attr = 'default'
 
-        alerts_count = len(self.contact_list.getAlerts(entity, use_bare_jid=use_bare_jid))
-        if with_alert and alerts_count:
-            entity_attr = 'alert'
-            header = C.ALERT_HEADER % alerts_count
+        notifs = self.host.getNotifs(entity.bare, profile=self.profile)
+        if notifs:
+            entity_attr = 'cl_notifs'
+            header = u'({}) '.format(len(notifs))
         else:
-            header = ''
+            header = u''
 
         markup.append((entity_attr, entity_txt))
         if markup_prepend:
--- a/frontends/src/primitivus/game_tarot.py	Mon Jun 27 21:45:13 2016 +0200
+++ b/frontends/src/primitivus/game_tarot.py	Mon Jun 27 22:36:22 2016 +0200
@@ -294,7 +294,7 @@
         QuickTarotGame.tarotGameInvalidCardsHandler(self, phase, played_cards, invalid_cards)
         self.hand_wid.update(self.hand)
         if self._autoplay == None:  # No dialog if there is autoplay
-            self.parent.host.notify(_('Cards played are invalid !'))
+            self.parent.host.barNotify(_('Cards played are invalid !'))
         self.parent.host.redraw()
 
     def tarotGameCardsPlayedHandler(self, player, cards):
--- a/frontends/src/primitivus/primitivus	Mon Jun 27 21:45:13 2016 +0200
+++ b/frontends/src/primitivus/primitivus	Mon Jun 27 22:36:22 2016 +0200
@@ -586,11 +586,24 @@
         else:
             self.notif_bar.addPopUp(pop_up_widget)
 
-    def notify(self, message):
+    def barNotify(self, message):
         """"Notify message to user via notification bar"""
         self.notif_bar.addMessage(message)
         self.redraw()
 
+    def notify(self, type_, entity=None, message=None, subject=None, callback=None, cb_args=None, widget=None, profile=C.PROF_KEY_NONE):
+        if widget is None or widget is not None and widget != self.selected_widget:
+            # we ignore notification if the widget is selected but we can
+            # still do a desktop notification is the X window has not the focus
+            super(PrimitivusApp, self).notify(type_, entity, message, subject, callback, cb_args, widget, profile)
+        if not self.x_notify.hasFocus():
+            if message is None:
+                message = _("{app}: a new event has just happened{entity}").format(
+                    app=C.APP_NAME,
+                    entity=u' ({})'.format(entity) if entity else '')
+            self.x_notify.sendNotification(message)
+
+
     def newWidget(self, widget):
         if self.selected_widget is None:
             self.selectWidget(widget)
@@ -651,6 +664,7 @@
             # we have clicked on a private MUC conversation
             chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, force_hash = Chat.getPrivateHash(contact_list.profile, entity), profile=contact_list.profile)
         else:
+            self.clearNotifs(entity, profile=contact_list.profile)
             chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, profile=contact_list.profile)
         self.selectWidget(chat_widget)
         self.menu_roller.addMenu(_('Chat menu'), chat_widget.getMenu(), C.MENU_ID_WIDGET)
--- a/frontends/src/quick_frontend/constants.py	Mon Jun 27 21:45:13 2016 +0200
+++ b/frontends/src/quick_frontend/constants.py	Mon Jun 27 22:36:22 2016 +0200
@@ -63,10 +63,6 @@
         "paused": u"⦷"
     }
 
-    # Alerts
-    ALERT_MESSAGE = "MESSAGE" # New message received
-    ALERT_NICK = "NICK" # our nickname was mentionned
-
     # Blogs
     ENTRY_MODE_TEXT = "text"
     ENTRY_MODE_RICH = "rich"
@@ -86,4 +82,10 @@
     UPDATE_SELECTION = 'SELECTION'
     UPDATE_STRUCTURE = 'STRUCTURE' # high level update (i.e. not item level but organisation of items)
 
-    LISTENERS = {'avatar', 'nick', 'presence', 'profilePlugged', 'disconnect', 'gotMenus', 'menu'}
+    LISTENERS = {'avatar', 'nick', 'presence', 'profilePlugged', 'disconnect', 'gotMenus', 'menu', 'notification', 'notificationsClear'}
+
+    # Notifications
+    NOTIFY_MESSAGE = 'MESSAGE'  # a message was received
+    NOTIFY_MENTION = 'MENTION'  # user was mentionned
+    NOTIFY_PROGRESS_END = 'PROGRESS_END'  # a progression has finised
+    NOTIFY_GENERIC = 'GENERIC'  # a notification which has not its own type
--- a/frontends/src/quick_frontend/quick_app.py	Mon Jun 27 21:45:13 2016 +0200
+++ b/frontends/src/quick_frontend/quick_app.py	Mon Jun 27 22:36:22 2016 +0200
@@ -34,6 +34,7 @@
 
 import sys
 from collections import OrderedDict
+import time
 
 try:
     # FIXME: to be removed when an acceptable solution is here
@@ -51,13 +52,7 @@
     def __init__(self, profile):
         self.profile = profile
         self.whoami = None
-        self.data = {}
-
-    def __getitem__(self, key):
-        return self.data[key]
-
-    def __setitem__(self, key, value):
-        self.data[key] = value
+        self.notifications = {}  # key: bare jid or '' for general, value: notif data
 
     def plug(self):
         """Plug the profile to the host"""
@@ -268,6 +263,8 @@
 
         self.current_action_ids = set() # FIXME: to be removed
         self.current_action_ids_cb = {} # FIXME: to be removed
+        self._notif_id = 0
+        self._notifications = OrderedDict()
         self.media_dir = self.bridge.getConfig('', 'media_dir')
         self.features = None
 
@@ -284,12 +281,6 @@
         """widgets currently visible (must be implemented by frontend)"""
         raise NotImplementedError
 
-    @property
-    def alerts_count(self):
-        """Count the over whole alerts for all contact lists"""
-        # FIXME
-        # return sum([sum(clist._alerts.values()) for clist in self.contact_lists.values()])
-
     def registerSignal(self, function_name, handler=None, iface="core", with_profile=True):
         """Register a handler for a signal
 
@@ -330,6 +321,10 @@
                 args: (entity, new_nick, profile)
             - presence: called when a presence is received
                 args: (entity, show, priority, statuses, profile)
+            - notification: called when a new notification is emited
+                args: (entity, notification_data, profile)
+            - notification_clear: called when notifications are cleared
+                args: (entity, type_, profile)
             - menu: called when a menu item is added or removed
                 args: (type_, path, path_i18n, item) were values are:
                     type_: same as in [sat.core.sat_main.SAT.importMenu]
@@ -496,19 +491,6 @@
 
         chat_widget.messageNew(uid, timestamp, from_jid, target, msg, subject, type_, extra, profile)
 
-        # ContactList alert
-        if not from_me:
-            visible = False
-            for widget in self.visible_widgets:
-                if isinstance(widget, quick_chat.QuickChat) and widget.manageMessage(from_jid, type_):
-                    visible = True
-                    break
-            if visible: # FIXME: à virer gof:
-                if self.isHidden():  # the window is hidden
-                    self.updateAlertsCounter(extra_inc=1)
-            else:
-                contact_list.addAlert(from_jid.bare if type_ == C.MESS_TYPE_GROUPCHAT else from_jid)
-
     def messageSend(self, to_jid, message, subject=None, mess_type="auto", extra=None, callback=None, errback=None, profile_key=C.PROF_KEY_NONE):
         if subject is None:
             subject = {}
@@ -602,6 +584,76 @@
         #         contact_list.setCache(from_jid, 'chat_state', to_display)
         #         widget.update(from_jid)
 
+    def notify(self, type_, entity=None, message=None, subject=None, callback=None, cb_args=None, widget=None, profile=C.PROF_KEY_NONE):
+        """Trigger an event notification
+
+        @param type_(unicode): notifation kind,
+            one of C.NOTIFY_* constant or any custom type specific to frontend
+        @param entity(jid.JID, None): entity involved in the notification
+            if entity is in contact list, a indicator may be added in front of it
+        @param message(unicode, None): message of the notification
+        @param subject(unicode, None): subject of the notification
+        @param callback(callable, None): method to call when notification is selected
+        @param cb_args(list, None): list of args for callback
+        @param widget(object, None): widget where the notification happened
+        """
+        notif_dict = self.profiles[profile].notifications
+        key = '' if entity is None else entity.bare
+        type_notifs = notif_dict.setdefault(key, {}).setdefault(type_, [])
+        notif_data = {
+            'id': self._notif_id,
+            'time': time.time(),
+            'entity': entity,
+            'callback': callback,
+            'cb_args': cb_args,
+            'message': message,
+            'subject': subject,
+            }
+        if widget is not None:
+            notif_data[widget] = widget
+        type_notifs.append(notif_data)
+        self._notifications[self._notif_id] = notif_data
+        self.callListeners('notification', entity, notif_data, profile=profile)
+
+    def getNotifs(self, entity, type_=None, profile=C.PROF_KEY_NONE):
+        """return notifications for given entity
+
+        @param entity(jid.JID, None): bare jid of the entity to check
+            None to get general notifications
+        @param type_(unicode, None): notification type to filter
+            None to get all notifications
+        @return (list[dict]): list of notifications
+        """
+        notif_dict = self.profiles[profile].notifications
+        key = '' if entity is None else entity.bare
+        key_notifs = notif_dict.setdefault(key, {})
+        if type_ is not None:
+            return key_notifs.get(type_, [])
+        ret = []
+        for notifs_list in key_notifs.itervalues():
+            ret.extend(notifs_list)
+        return ret
+
+    def clearNotifs(self, entity, type_=None, profile=C.PROF_KEY_NONE):
+        """return notifications for given entity
+
+        @param entity(jid.JID, None): bare jid of the entity to check
+            None to clear general notifications (but keep entities ones)
+        @param type_(unicode, None): notification type to filter
+            None to clear all notifications
+        @return (list[dict]): list of notifications
+        """
+        notif_dict = self.profiles[profile].notifications
+        key = '' if entity is None else entity.bare
+        try:
+            if type_ is None:
+                del notif_dict[key]
+            else:
+                del notif_dict[key][type_]
+        except KeyError:
+            return
+        self.callListeners('notificationsClear', entity, type_, profile=profile)
+
     def psEventHandler(self, category, service_s, node, event_type, data, profile):
         """Called when a PubSub event is received.
 
@@ -688,13 +740,6 @@
         """
         raise NotImplementedError
 
-    def updateAlertsCounter(self, extra_inc=0):
-        """Update the over whole alerts counter.
-
-        @param extra_inc (int): extra counter
-        """
-        pass
-
     def paramUpdateHandler(self, name, value, namespace, profile):
         log.debug(_(u"param update: [%(namespace)s] %(name)s = %(value)s") % {'namespace': namespace, 'name': name, 'value': value})
         if (namespace, name) == ("Connection", "JabberID"):
--- a/frontends/src/quick_frontend/quick_chat.py	Mon Jun 27 21:45:13 2016 +0200
+++ b/frontends/src/quick_frontend/quick_chat.py	Mon Jun 27 22:36:22 2016 +0200
@@ -340,15 +340,6 @@
         """
         raise NotImplementedError
 
-    def notify(self, contact="somebody", msg=""):
-        """Notify the user of a new message if the frontend doesn't have the focus.
-
-        @param contact (unicode): contact who wrote to the users
-        @param msg (unicode): the message that has been received
-        """
-        # FIXME: not called anymore after refactoring
-        raise NotImplemented
-
     def printDayChange(self, day):
         """Display the day on a new line.
 
--- a/frontends/src/quick_frontend/quick_contact_list.py	Mon Jun 27 21:45:13 2016 +0200
+++ b/frontends/src/quick_frontend/quick_contact_list.py	Mon Jun 27 22:36:22 2016 +0200
@@ -68,9 +68,6 @@
         # contacts in roster (bare jids)
         self._roster = set()
 
-        # alerts per entity (key: full jid, value: list of alerts)
-        self._alerts = {}
-
         # selected entities, full jid
         self._selected = set()
 
@@ -91,6 +88,10 @@
         self.host.addListener('presence', self.presenceListener, [self.profile])
         self.nickListener = self.onNickUpdate
         self.host.addListener('nick', self.nickListener, [self.profile])
+        self.notifListener = self.onNotification
+        self.host.addListener('notification', self.notifListener, [self.profile])
+        # notifListener only update the entity, so we can re-use it
+        self.host.addListener('notificationsClear', self.notifListener, [self.profile])
 
     def _showEmptyGroups(self, show_str):
         # Called only by __init__
@@ -321,8 +322,6 @@
         self._specials.clear()
         self._special_extras.clear()
         self._roster.clear()
-        self._alerts.clear()
-        self.host.updateAlertsCounter()
         self.update()
 
     def setContact(self, entity, groups=None, attributes=None, in_roster=False):
@@ -392,15 +391,14 @@
         show = self.getCache(entity, C.PRESENCE_SHOW)
 
         if check_resource:
-            alerts = self._alerts.keys()
             selected = self._selected
         else:
-            alerts = {alert.bare for alert in self._alerts}
             selected = {selected.bare for selected in self._selected}
         return ((show is not None and show != C.PRESENCE_UNAVAILABLE)
                 or self.show_disconnected
-                or entity in alerts
-                or entity in selected)
+                or entity in selected
+                or self.host.getNotifs(entity.bare, profile=self.profile)
+                )
 
     def anyEntityToShow(self, entities, check_resources=False):
         """Tell if in a list of entities, at least one should be shown
@@ -443,16 +441,12 @@
             self._groups[group]['jids'].remove(entity_bare)
             if not self._groups[group]['jids']:
                 self._groups.pop(group) # FIXME: we use pop because of pyjamas: http://wiki.goffi.org/wiki/Issues_with_Pyjamas/en
-        for iterable in (self._selected, self._alerts, self._specials, self._special_extras):
+        for iterable in (self._selected, self._specials, self._special_extras):
             to_remove = set()
             for set_entity in iterable:
                 if set_entity.bare == entity.bare:
                     to_remove.add(set_entity)
-            if isinstance(iterable, set):
-                iterable.difference_update(to_remove)
-            else:  # XXX: self._alerts is a dict
-                for item in to_remove:
-                    del iterable[item]
+            iterable.difference_update(to_remove)
         self.update([entity], C.UPDATE_DELETE, self.profile)
 
     def onPresenceUpdate(self, entity, show, priority, statuses, profile):
@@ -500,6 +494,17 @@
         self.setCache(entity, 'nick', new_nick)
         self.update([entity], C.UPDATE_MODIFY, profile)
 
+    def onNotification(self, entity, notif, profile):
+        """Update entity with notification
+
+        @param entity(jid.JID): entity updated
+        @param notif(dict): notification data
+        @param profile: %(doc_profile)s
+        """
+        assert profile == self.profile
+        if entity is not None:
+            self.update([entity], C.UPDATE_MODIFY, profile)
+
     def unselect(self, entity):
         """Unselect an entity
 
@@ -540,66 +545,6 @@
                 self._selected.add(entity)
                 self.update([entity], C.UPDATE_SELECTION, profile=self.profile)
 
-    def getAlerts(self, entity, use_bare_jid=False, filter_=None):
-        """Return alerts set to this entity.
-
-        @param entity (jid.JID): entity
-        @param use_bare_jid (bool): if True, cumulate the alerts of all the resources sharing the same bare JID
-        @param filter_(iterable, None): alert to take into account,
-            None to count all of them
-        @return (list[unicode,None]): list of C.ALERT_* or None for undefined ones
-        """
-        return [] # FIXME: temporarily disabled
-        if not use_bare_jid:
-            alerts = self._alerts.get(entity, [])
-        else:
-            alerts = []
-            for contact, contact_alerts in self._alerts:
-                if contact.bare == entity:
-                    alerts.extend(contact_alerts)
-        if filter_ is None:
-            return alerts
-        else:
-            return [alert for alert in alerts if alert in filter_]
-
-    def addAlert(self, entity, type_=None):
-        """Add an alert for this enity
-
-        @param entity(jid.JID): entity who received an alert (resource is significant)
-        @param type_(unicode, None): type of alert (C.ALERT_*)
-            None for generic alert
-        """
-        self._alerts.setdefault(entity, [])
-        self._alerts[entity].append(type_)
-        self.update([entity], C.UPDATE_MODIFY, self.profile)
-        self.host.updateAlertsCounter() # FIXME: ?
-
-    def removeAlerts(self, entity, use_bare_jid=True):
-        """Eventually remove an alert on the entity (usually for a waiting message).
-
-        @param entity(jid.JID): entity (resource is significant)
-        @param use_bare_jid (bool): if True, ignore the resource
-        """
-        if use_bare_jid:
-            to_remove = set()
-            for alert_entity in self._alerts:
-                if alert_entity.bare == entity.bare:
-                    to_remove.add(alert_entity)
-            if not to_remove:
-                return  # nothing changed
-            for entity in to_remove:
-                del self._alerts[entity]
-            self.update([to_remove], C.UPDATE_MODIFY, self.profile)
-            self.host.updateAlertsCounter() # FIXME: ?
-        else:
-            try:
-                del self._alerts[entity]
-            except KeyError:
-                return  # nothing changed
-            else:
-                self.update([entity], C.UPDATE_MODIFY, self.profile)
-            self.host.updateAlertsCounter() # FIXME: ?
-
     def showOfflineContacts(self, show):
         """Tell if offline contacts should shown